开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

数据库索引,终于懂了

发表于 2021-03-24

不少朋友留言问MySQL索引底层的实现,让我讲讲B+树。知其然,知其所以然,讲懂B+树其实不难,今天更多聊聊“数据库索引,为什么设计成这样”。

问题1. 数据库为什么要设计索引?

图书馆存了1000W本图书,要从中找到《架构师之路》,一本本查,要查到什么时候去?

于是,图书管理员设计了一套规则:

(1)一楼放历史类,二楼放文学类,三楼放IT类…

(2)IT类,又分软件类,硬件类…

(3)软件类,又按照书名排序…

以便快速找到一本书。

与之类比,数据库存储了1000W条数据,要从中找到name=”shenjian”的记录,一条条查,要查到什么时候去?

于是,要有索引,用于提升数据库的查找速度。

问题2. 哈希(hash)比树(tree)更快,索引结构为什么要设计成树型?

加速查找速度的数据结构,常见的有两类:

(1)哈希,例如HashMap,查询/插入/修改/删除的平均时间复杂度都是O(1);

(2)树,例如平衡二叉搜索树,查询/插入/修改/删除的平均时间复杂度都是O(lg(n));

可以看到,不管是读请求,还是写请求,哈希类型的索引,都要比树型的索引更快一些,那为什么,索引结构要设计成树型呢?

画外音:80%的同学,面试都答不出来。

索引设计成树形,和SQL的需求相关。

对于这样一个单行查询的SQL需求:

select \ from t where name=”shenjian”;*

确实是哈希索引更快,因为每次都只查询一条记录。

画外音:所以,如果业务需求都是单行访问,例如passport,确实可以使用哈希索引。

但是对于排序查询的SQL需求:

(1)分组:group by

(2)排序:order by

(3)比较:<、>

(4)…

哈希型的索引,时间复杂度会退化为O(n),而树型的“有序”特性,依然能够保持O(log(n)) 的高效率。

任何脱离需求的设计都是耍流氓。

多说一句,InnoDB并不支持手动建立哈希索引。

画外音:自适应hash索引,是InnoDB内核机制。

问题3. 数据库索引为什么使用B+树?

为了保持知识体系的完整性,简单介绍下几种树。

第一种:二叉搜索树

二叉搜索树,如上图,是最为大家所熟知的一种数据结构,就不展开介绍了,它为什么不适合用作数据库索引?

(1)当数据量大的时候,树的高度会比较高,数据量大的时候,查询会比较慢;

(2)每个节点只存储一个记录,可能导致一次查询有很多次磁盘IO;

画外音:这个树经常出现在大学课本里,所以最为大家所熟知。

第二种:B树

B树,如上图,它的特点是:

(1)不再是二叉搜索,而是m叉搜索;

(2)叶子节点,非叶子节点,都存储数据;

(3)中序遍历,可以获得所有节点;

画外音,实在不想介绍这个特性:非根节点包含的关键字个数j满足, (┌m/2┐)-1 <= j <= m-1 ,节点分裂时要满足这个条件。

B树被作为实现索引的数据结构被创造出来,是因为它能够完美的利用“局部性原理”。

什么是局部性原理?

局部性原理的逻辑是这样的:

(1)内存读写块,磁盘读写慢,而且慢很多;

(2)磁盘预读:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,如果未来要读取的数据就在这一页中,可以避免未来的磁盘IO,提高效率;

画外音:通常,操作系统一页数据是4K,MySQL 的一页是16K。

(3)局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO;

B树为何适合做索引?

(1)由于是m分叉的,高度能够大大降低;

(2)每个节点可以存储j个记录,如果将节点大小设置为页大小,例如4K,能够充分的利用预读的特性,极大减少磁盘IO;

第三种:B+树

B+树,如上图,仍是m叉搜索树,在B树的基础上,做了一些改进:

(1)非叶子节点不再存储数据,数据只存储在同一层的叶子节点上;

画外音:B+树中根到每一个节点的路径长度一样,而B树不是这样。

(2)叶子之间,增加了链表,获取所有节点,不再需要中序遍历;

这些改进让B+树比B树有更优的特性:

(1)范围查找,定位min与max之后,中间叶子节点,就是结果集,不用中序回溯;

画外音:范围查询在SQL中用得很多,这是B+树比B树最大的优势。

(2)叶子节点存储实际记录行,记录行相对比较紧密的存储,适合大数据量磁盘存储;非叶子节点存储记录的PK,用于查询加速,适合内存存储;

(3)非叶子节点,不存储实际记录,而只存储记录的KEY的话,那么在相同内存的情况下,B+树能够存储更多索引;

最后,量化说下,为什么m叉的B+树比二叉搜索树的高度大大大大降低?

大概计算一下:

(1)局部性原理,将一个节点的大小设为一页,一页4K,假设一个KEY有8字节,一个节点可以存储500个KEY,即j=500;

(2)m叉树,大概m/2<= j <=m,即可以差不多是1000叉树;

(3)那么:

一层树:1个节点,1*500个KEY,大小4K

二层树:1000个节点,1000*500=50W个KEY,大小1000*4K=4M

三层树:1000*1000个节点,1000*1000*500=5亿个KEY,大小1000*1000*4K=4G

画外音:额,帮忙看下有没有算错。

可以看到,存储大量的数据(5亿),并不需要太高树的深度(高度3),索引也不是太占内存(4G)。

总结

(1)数据库索引用于加速查询;

(2)虽然哈希索引是O(1),树索引是O(log(n)),但SQL有很多“有序”需求,故数据库使用树型索引;

(3)InnoDB不支持手动创建哈希索引;

(4)数据预读的思路是:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,以便未来减少磁盘IO

(5)局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO

(5)数据库的索引最常用B+树:

  • 很适合磁盘存储,能够充分利用局部性原理,磁盘预读;
  • 很低的树高度,能够存储大量数据;
  • 索引本身占用的内存很小;
  • 能够很好的支持单点查询,范围查询,有序性查询;

架构师之路-分享可落地的架构文章

相关推荐:\

《InnoDB并发如此高,原因竟然在这?》

作业:\

同样是B+树,InnoDB和MyISAM的索引有什么不同呢?

思路比结论更重要,希望你有收获,谢转。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot的四种异步处理,写这篇文章,我自己先学到

发表于 2021-03-24

最近更新了一系列关于异步和回调的文章,比如《一篇文章,搞明白异步和多线程的区别》、《两个经典例子让你彻底理解java回调机制》、《异步请求和异步调用有区别?》,大家感兴趣的话可温习一下。

今天再带大家学习汇总一下SpringBoot中异步处理的4种形式,下面开始正文:

前言

在网络上有关于SpringBoot的异步请求和异步调有两种说法,经过调用这两种说法本质上就是一回事,在《异步请求和异步调用有区别?》一种,已经做过解释了。

同时,我们也知道了“服务实现的异步与同步特性完全独立于客户端调用的异步和同步特性。也就是说客户端可以异步的去调用同步服务,而且客户端也可以同步的去调用异步服务。”

本篇文章我们以SpringBoot中异步的使用(包括:异步调用和异步方法两个维度)来进行讲解。

异步请求与同步请求

我们先通过一张图来区分一下异步请求和同步请求的区别:

在这里插入图片描述

在上图中有三个角色:客户端、Web容器和业务处理线程。

两个流程中客户端对Web容器的请求,都是同步的。因为它们在请求客户端时都处于阻塞等待状态,并没有进行异步处理。

在Web容器部分,第一个流程采用同步请求,第二个流程采用异步回调的形式。

通过异步处理,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增加了服务器对客户端请求的吞吐量。但并发请求量较大时,通常会通过负载均衡的方案来解决,而不是异步。

Servlet3.0中的异步

Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由一个线程从头到尾处理。当涉及到耗时操作时,性能问题便比较明显。

Servlet 3.0中提供了异步处理请求。可以先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增加服务的吞吐量。

Servlet 3.0的异步是通过AsyncContext对象来完成的,它可以从当前线程传给另一个线程,并归还初始线程。新的线程处理完业务可以直接返回结果给客户端。

AsyncContext对象可以从HttpServletRequest中获取:

1
2
3
4
java复制代码@RequestMapping("/async")
public void async(HttpServletRequest request) {
AsyncContext asyncContext = request.getAsyncContext();
}

在AsyncContext中提供了获取ServletRequest、ServletResponse和添加监听(addListener)等功能:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public interface AsyncContext {

ServletRequest getRequest();

ServletResponse getResponse();

void addListener(AsyncListener var1);

void setTimeout(long var1);

// 省略其他方法
}

不仅可以通过AsyncContext获取Request和Response等信息,还可以设置异步处理超时时间。通常,超时时间(单位毫秒)是需要设置的,不然无限等下去不就与同步处理一样了。

通过AsyncContext的addListener还可以添加监听事件,用来处理异步线程的开始、完成、异常、超时等事件回调。

addListener方法的参数AsyncListener的源码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码public interface AsyncListener extends EventListener {
// 异步执行完毕时调用
void onComplete(AsyncEvent var1) throws IOException;
// 异步线程执行超时调用
void onTimeout(AsyncEvent var1) throws IOException;
// 异步线程出错时调用
void onError(AsyncEvent var1) throws IOException;
// 异步线程开始时调用
void onStartAsync(AsyncEvent var1) throws IOException;
}

通常,异常或超时时返回调用方错误信息,而异常时会处理一些清理和关闭操作或记录异常日志等。

基于Servlet方式实现异步请求

下面直接看一个基于Servlet方式的异步请求示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
csharp复制代码@GetMapping(value = "/email/send")
public void servletReq(HttpServletRequest request) {
AsyncContext asyncContext = request.startAsync();
// 设置监听器:可设置其开始、完成、异常、超时等事件的回调处理
asyncContext.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) {
System.out.println("处理超时了...");
}

@Override
public void onStartAsync(AsyncEvent event) {
System.out.println("线程开始执行");
}

@Override
public void onError(AsyncEvent event) {
System.out.println("执行过程中发生错误:" + event.getThrowable().getMessage());
}

@Override
public void onComplete(AsyncEvent event) {
System.out.println("执行完成,释放资源");
}
});
//设置超时时间
asyncContext.setTimeout(6000);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println("内部线程:" + Thread.currentThread().getName());
asyncContext.getResponse().getWriter().println("async processing");
} catch (Exception e) {
System.out.println("异步处理发生异常:" + e.getMessage());
}
// 异步请求完成通知,整个请求完成
asyncContext.complete();
}
});
//此时request的线程连接已经释放了
System.out.println("主线程:" + Thread.currentThread().getName());
}

启动项目,访问对应的URL,打印日志如下:

1
2
3
perl复制代码主线程:http-nio-8080-exec-4
内部线程:http-nio-8080-exec-5
执行完成,释放资源

可以看出,上述代码先执行完了主线程,也就是程序的最后一行代码的日志打印,然后才是内部线程的执行。内部线程执行完成,AsyncContext的onComplete方法被调用。

如果通过浏览器访问对应的URL,还可以看到该方法的返回值“async processing”。说明内部线程的结果同样正常的返回到客户端了。

基于Spring实现异步请求

基于Spring可以通过Callable、DeferredResult或者WebAsyncTask等方式实现异步请求。

基于Callable实现

对于一次请求(/email),基于Callable的处理流程如下:

1、Spring MVC开启副线程处理业务(将Callable提交到TaskExecutor);

2、DispatcherServlet和所有的Filter退出Web容器的线程,但是response保持打开状态;

3、Callable返回结果,SpringMVC将原始请求重新派发给容器(再重新请求一次/email),恢复之前的处理;

4、DispatcherServlet重新被调用,将结果返回给用户;

代码实现示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码@GetMapping("/email")
public Callable<String> order() {
System.out.println("主线程开始:" + Thread.currentThread().getName());
Callable<String> result = () -> {
System.out.println("副线程开始:" + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("副线程返回:" + Thread.currentThread().getName());
return "success";
};

System.out.println("主线程返回:" + Thread.currentThread().getName());
return result;
}

访问对应URL,控制台输入日志如下:

1
2
3
4
perl复制代码主线程开始:http-nio-8080-exec-1
主线程返回:http-nio-8080-exec-1
副线程开始:task-1
副线程返回:task-1

通过日志可以看出,主线程已经完成了,副线程才进行执行。同时,URL返回结果“success”。这也说明一个问题,服务器端的异步处理对客户端来说是不可见的。

Callable默认使用SimpleAsyncTaskExecutor类来执行,这个类非常简单而且没有重用线程。在实践中,需要使用AsyncTaskExecutor类来对线程进行配置。

这里通过实现WebMvcConfigurer接口来完成线程池的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
scss复制代码@Configuration
public class WebConfig implements WebMvcConfigurer {

@Resource
private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;

/**
* 配置线程池
*/
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("thread-pool-");
// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}

@Override
public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
// 处理callable超时
configurer.setDefaultTimeout(60 * 1000);
configurer.setTaskExecutor(myThreadPoolTaskExecutor);
configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
}

@Bean
public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
return new TimeoutCallableProcessingInterceptor();
}
}

为了验证打印的线程,我们将实例代码中的System.out.println替换成日志输出,会发现在使用线程池之前,打印日志如下:

1
2
3
4
yaml复制代码2021-02-21 09:45:37.144  INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始:http-nio-8080-exec-1
2021-02-21 09:45:37.144 INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程返回:http-nio-8080-exec-1
2021-02-21 09:45:37.148 INFO 8312 --- [ task-1] c.s.learn.controller.AsynController : 副线程开始:task-1
2021-02-21 09:45:38.153 INFO 8312 --- [ task-1] c.s.learn.controller.AsynController : 副线程返回:task-1

线程名称为“task-1”。让线程池生效之后,打印日志如下:

1
2
3
4
yaml复制代码2021-02-21 09:50:28.950  INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始:http-nio-8080-exec-1
2021-02-21 09:50:28.951 INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程返回:http-nio-8080-exec-1
2021-02-21 09:50:28.955 INFO 8339 --- [ thread-pool-1] c.s.learn.controller.AsynController : 副线程开始:thread-pool-1
2021-02-21 09:50:29.956 INFO 8339 --- [ thread-pool-1] c.s.learn.controller.AsynController : 副线程返回:thread-pool-1

线程名称为“thread-pool-1”,其中前面的“thread-pool”正是我们配置的线程池前缀。

除了线程池的配置,还可以配置统一异常处理,这里就不再演示了。

基于WebAsyncTask实现

Spring提供的WebAsyncTask是对Callable的包装,提供了更强大的功能,比如:处理超时回调、错误回调、完成回调等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typescript复制代码@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask() {
log.info("外部线程:" + Thread.currentThread().getName());
WebAsyncTask<String> result = new WebAsyncTask<>(60 * 1000L, new Callable<String>() {
@Override
public String call() {
log.info("内部线程:" + Thread.currentThread().getName());
return "success";
}
});
result.onTimeout(new Callable<String>() {
@Override
public String call() {
log.info("timeout callback");
return "timeout callback";
}
});
result.onCompletion(new Runnable() {
@Override
public void run() {
log.info("finish callback");
}
});
return result;
}

访问对应请求,打印日志:

1
2
3
yaml复制代码2021-02-21 10:22:33.028  INFO 8547 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 外部线程:http-nio-8080-exec-1
2021-02-21 10:22:33.033 INFO 8547 --- [ thread-pool-1] c.s.learn.controller.AsynController : 内部线程:thread-pool-1
2021-02-21 10:22:33.055 INFO 8547 --- [nio-8080-exec-2] c.s.learn.controller.AsynController : finish callback

基于DeferredResult实现

DeferredResult使用方式与Callable类似,但在返回结果时不一样,它返回的时实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。

DeferredResult的这个特性对实现服务端推技术、订单过期时间处理、长轮询、模拟MQ的功能等高级应用非常重要。

关于DeferredResult的使用先来看一下官方的例子和说明:

1
2
3
4
5
6
7
8
9
10
less复制代码@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult in in-memory queue ...
return deferredResult;
}

// In some other thread...
deferredResult.setResult(data);

上述示例中我们可以发现DeferredResult的调用并不一定在Spring MVC当中,它可以是别的线程。官方的解释也是如此:

In this case the return value will also be produced from a separate thread. However, that thread is not known to Spring MVC. For example the result may be produced in response to some external event such as a JMS message, a scheduled task, etc.

也就是说,DeferredResult返回的结果也可能是由MQ、定时任务或其他线程触发。来个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
swift复制代码@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {

private List<DeferredResult<String>> deferredResultList = new ArrayList<>();

@ResponseBody
@GetMapping("/hello")
public DeferredResult<String> helloGet() throws Exception {
DeferredResult<String> deferredResult = new DeferredResult<>();

//先存起来,等待触发
deferredResultList.add(deferredResult);
return deferredResult;
}

@ResponseBody
@GetMapping("/setHelloToAll")
public void helloSet() throws Exception {
// 让所有hold住的请求给与响应
deferredResultList.forEach(d -> d.setResult("say hello to all"));
}
}

第一个请求/hello,会先将deferredResult存起来,前端页面是一直等待(转圈)状态。直到发第二个请求:setHelloToAll,所有的相关页面才会有响应。

整个执行流程如下:

  • controller返回一个DeferredResult,把它保存到内存里或者List里面(供后续访问);
  • Spring MVC调用request.startAsync(),开启异步处理;

与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态;

  • 应用通过另外一个线程(可能是MQ消息、定时任务等)给DeferredResult#setResult值。然后SpringMVC会把这个请求再次派发给servlet容器;
  • DispatcherServlet再次被调用,然后处理后续的标准流程;

通过上述流程可以发现:利用DeferredResult可实现一些长连接的功能,比如当某个操作是异步时,可以先保存对应的DeferredResult对象,当异步通知回来时,再找到这个DeferredResult对象,在setResult处理结果即可。从而提高性能。

SpringBoot中的异步实现

在SpringBoot中将一个方法声明为异步方法非常简单,只需两个注解即可@EnableAsync和@Async。其中@EnableAsync用于开启SpringBoot支持异步的功能,用在SpringBoot的启动类上。@Async用于方法上,标记该方法为异步处理方法。

需要注意的是@Async并不支持用于被@Configuration注解的类的方法上。同一个类中,一个方法调用另外一个有@Async的方法,注解也是不会生效的。

@EnableAsync的使用示例:

1
2
3
4
5
6
7
8
less复制代码@SpringBootApplication
@EnableAsync
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

@Async的使用示例:

1
2
3
4
5
6
7
8
typescript复制代码@Service
public class SyncService {

@Async
public void asyncEvent() {
// 业务处理
}
}

@Async注解的使用与Callable有类似之处,在默认情况下使用的都是SimpleAsyncTaskExecutor线程池,可参考Callable中的方式来自定义线程池。

下面通过一个实例来验证一下,启动类上使用@EnableAsync,然后定义Controller类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
public class IndexController {

@Resource
private UserService userService;

@RequestMapping("/async")
public String async(){
System.out.println("--IndexController--1");
userService.sendSms();
System.out.println("--IndexController--4");
return "success";
}
}

定义Service及异步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码@Service
public class UserService {

@Async
public void sendSms(){
System.out.println("--sendSms--2");
IntStream.range(0, 5).forEach(d -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("--sendSms--3");
}
}

如果先注释掉@EnableAsync和@Async注解,即正常情况下的业务请求,打印日志为:

1
2
3
4
css复制代码--IndexController--1
--sendSms--2
--sendSms--3
--IndexController--4

使用@EnableAsync和@Async注解时,打印日志如下:

1
2
3
4
css复制代码--IndexController--1
--IndexController--4
--sendSms--2
--sendSms--3

通过日志的对比我们可以看出,使用了@Async的方法,会被当成一个子线程。所以,整个sendSms方法会在主线程执行完了之后执行。

这样的效果是不是跟我们上面使用的其他形式的异步异曲同工?所以在文章最开始已经说到,网络上所谓的“异步调用与异步请求的区别”是并不存储在的,本质上都是一回事,只不过实现形式不同而已。这里所提到异步方法,也就是将方法进行异步处理而已。

@Async、WebAsyncTask、Callable、DeferredResult的区别

所在的包不同:

  • @Async:org.springframework.scheduling.annotation;
  • WebAsyncTask:org.springframework.web.context.request.async;
  • Callable:java.util.concurrent;
  • DeferredResult:org.springframework.web.context.request.async;

通过所在的包,我们应该隐隐约约感到一些区别,比如@Async是位于scheduling包中,而WebAsyncTask和DeferredResult是用于Web(Spring MVC)的,而Callable是用于concurrent(并发)处理的。

对于Callable,通常用于Controller方法的异步请求,当然也可以用于替换Runable的方式。在方法的返回上与正常的方法有所区别:

1
2
3
4
5
6
7
typescript复制代码// 普通方法
public String aMethod(){
}

// 对照Callable方法
public Callable<String> aMethod(){
}

而WebAsyncTask是对Callable的封装,提供了一些事件回调的处理,本质上区别不大。

DeferredResult使用方式与Callable类似,重点在于跨线程之间的通信。

@Async也是替换Runable的一种方式,可以代替我们自己创建线程。而且适用的范围更广,并不局限于Controller层,而可以是任何层的方法上。

当然,大家也可以从返回结果,异常处理等角度来分析一下,这里就不再展开了。

小结

经过上述的一步步分析,大家应该对于Servlet3.0及Spring中对异步从处理有所了解。当了解了这些基础理论,实战实例,使用方法及注意事项之后,想必更能够对网络上的相关知识能够进一步的去伪存真。

尽信书则不如无书,带大家一起学习,一起研究,一起去伪存真,追求真正有用的知识。

原文链接:《SpringBoot的四种异步处理,写这篇文章,我自己先学到了》


程序新视界

\

公众号“ 程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料

\

微信公众号:程序新视界

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mysql中FIND_IN_SET函数用法

发表于 2021-03-24

本篇文章主要介绍mysql中FIND_IN_SET函数用法,用来精确查询字段中以逗号分隔的数据

以及其与 like 和 in 的区别

1.问题发现

之前在做工作任务时有这么一个需求:需要用接口所传的服务商编号和所开通的产品类型查询这张表中是否有此信息来做返回结果。

但公司的产品类型有多个,每个服务商可能开通了多个不同的产品类型,存入产品类型时用的是一个字段,用逗号分隔开存储。

这种场景下就需要精确查找其产品类型,我一开始想的是用 in 和 like 来实现,但实际使用后并不是这种效果…请看下文讲解~

2.解决问题

使用 in 实现

sql语句如下:

1
2
3
4
5
6
7
8
mysql复制代码SELECT
agentNum
FROM
p4_agent_limit_merc_access a
WHERE
agentNum = #{agentNum} (传入的服务商编号)
AND #{productType} (传入的产品类型)
IN(a.productType);

但实际上这样写是查不到数据的,只有a.productType 字段的值等于传入的产品类型时(和IN前面的字符串完全匹配),这时查询才有效,否则是查不到结果的,即使a.productType 里面包含传入的产品类型。

使用 like 实现

sql语句如下:

1
2
3
4
5
6
7
8
mysql复制代码SELECT
agentNum
FROM
p4_agent_limit_merc_access a
WHERE
agentNum = #{agentNum}
AND a.productType
LIKE concat('%',#{productType}, '%');

表面看上去这样写是没问题的,但你细品一下,就知道问题很大,因为like是广泛的模糊查询,但一个字符串里包含你要传入的值时,也能查出来,比如我们的产品类型productType有QPOS和POS,然而此次查询的服务商只开通了QPOS产品,我却传入了POS这个类型,用上述语句查询后也能查出这条结果,这就是问题所在,所以用like查询出的范围会更广,这样明显不合理,不是我们想要的结果…

使用FIND_IN_SET函数实现

首先介绍下 FIND_IN_SET函数:

FIND_IN_SET(str,strlist)

str 要查询的字符串

strlist 字段名 参数以”,”分隔 如 (1,2,6,8)

查询字段(strlist)中包含(str)的结果,返回结果为null或记录

假如字符串str在由N个子链组成的字符串列表strlist 中,则返回值的范围在 1 到 N 之间。 一个字符串列表就是一个由一些被 ‘,’ 符号分开的子链组成的字符串。如果第一个参数是一个常数字符串,而第二个是type SET列,则FIND_IN_SET() 函数被优化,使用比特计算。 如果str不在strlist 或strlist 为空字符串,则返回值为 0 。如任意一个参数为NULL,则返回值为 NULL。这个函数在第一个参数包含一个逗号(‘,’)时将无法正常运行。

介绍简单了解下就好,具体看怎么用,我的sql如下:

1
2
3
4
5
6
7
mysql复制代码SELECT
agentNum
FROM
p4_agent_limit_merc_access a
WHERE
agentNum = #{agentNum}
AND FIND_IN_SET(#{productType},a.productType);

使用上述语句就可以精确查询出数据,实现需求。

再来看看FIND_IN_SET函数具体使用例子:

1
mysql复制代码SELECT FIND_IN_SET('b', 'a,b,c,d'); 返回2

因为b 在strlist集合中放在2的位置 从1开始

select FIND_IN_SET(‘1’, ‘1’); 返回 就是1 这时候的strlist集合有点特殊 只有一个字符串 其实就是要求前一个字符串 一定要在后一个字符串集合中 才返回 大于0的数

1
2
mysql复制代码select FIND_IN_SET('2', '1,2'); 返回2
select FIND_IN_SET('6', '1'); 返回0

3.总结

当 a.productType 字段是常量时,则可以用 in 来实现。

当其为变量时,则必须要用FIND_IN_SET函数来实现了。

 更多精彩功能请关注我的个人博客网站:liujian.cool

  欢迎关注我的个人公众号:程序猿刘川枫

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM学习笔记03-打破双亲委派机制(Tomcat) Tom

发表于 2021-03-24

Tomcat为何要打破双亲委派机制

开始之前,我们有个问题可以探讨一下:Tomcat使用默认的双亲委派类加载机制是否可行?

首先,我们可以思考一下,Tomcat作为一个web容器,它需要解决什么问题?

  1. 假如有若干个应用程序部署在Tomcat上,这些应用程序可能会依赖到同一第三方类库的不同版本,因此Tomcat必须支持每个应用程序的类库可以相互隔离
  2. 部署在同一个Tomcat上的不同应用程序,相同类库的相同版本应该是共享的,否则就会出现大量相同的类加载到虚拟机中
  3. Tomcat本身也有依赖的类库,与应用程序依赖的类库可能会混淆,基于安全考虑,应该将两者进行隔离
  4. 要支持Jsp文件修改后,其生成的class能在不重启的情况下及时被加载进JVM

那么,采用默认的双亲委派类加载机制,能否解决上述问题呢?

  • 问题1、3,如果Tomcat采用默认的双亲委派加载机制,是无法加载同一类库不同版本的类的,因为默认的双亲委派加载机制在加载类时,是通过类的全限定名做唯一性校验的
  • 问题2,默认的双亲委派类加载机制可以实现,因为它本就能保证唯一性
  • 问题4,我们知道Jsp文件更新其实也就是class文件更新了,此时类的全限定名并没有改变,修改Jsp文件后,类加载器会从方法区中直接取到已存在的,这会导致修改后Jsp文件其实不会重新加载。那么,如果直接卸载掉这个Jsp文件的类加载器,再重新创建类加载器去加载修改后的Jsp文件,不就能解决问题了吗?那么你应该能猜到每个Jsp文件应对应一个唯一的类加载器吧

到此,我们可以得出答案,Tomcat只使用默认的双亲委派类加载机制是不可行的!

Tomcat中的自定义类加载器

Tomcat的自定义类加载器 (1).png

Tomcat的几个主要的自定义类加载器

  • CommonClassLoader:公共的类加载器,其加载的class可以被Tomcat容器本身以及各个Webapp访问
  • CatalinaClassLoader:私有的类加载器,其加载的class对于Webapp不可见
  • ShareClassLoader:各个Webapp共享的类加载器,其加载的class对于所有Webapp可见,但对于Tomcat容器本身不可见
  • WebappClassLoader:各个Webapp私有的类加载器,其加载的class只对当前的Webapp可见

从上面的图,不难看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和ShareClassLoader使用,从而实现公有类库的公用,而CatalinaClassLoader和ShareClassLoader各自加载的类则与对方相互隔离
  • WebappClassLoader可以使用ShareClassLoader加载的类,但各个WebappClassLoader之间相互隔离
  • JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那个.class文件,一对一的设计是为了随时丢弃它,当Tomcat检测到JSP文件被修改时,会替换掉当前的JasperLoader的实例,并通过再一次建立一个新的JasperLoader实例来实现JSP文件的热加载功能

由此可知,Tomcat的设计是违背Java的双亲委派模型的,每个WebappClassLoader加载自己目录下的.class文件,不会传递给父加载器,这就打破了双亲委派机制,这样做正是为了实现隔离性。

下面这段代码模拟实现了Tomcat的WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码package org.laugen.jvm;
import sun.misc.PerfCounter;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class TestCustomizeClassLoader {
static class CustomizeClassLoader extends ClassLoader {
private String classPath;

public CustomizeClassLoader(String classPath) {
this.classPath = classPath;
}

// 读取class字节码文件
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
if (name.startsWith("org.laugen.jvm.Note")) {
c = findClass(name);
} else {
c = this.getParent().loadClass(name);
}
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

}

public static void main(String args[]) throws Exception {
CustomizeClassLoader classLoader1 = new CustomizeClassLoader("D:/MyClasses-v1");
System.out.println("自定义类加载器的父加载器:" + classLoader1.getParent().getClass().getName());
Class clazz1 = classLoader1.loadClass("org.laugen.jvm.Note");
Object obj1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("print", null);
method1.invoke(obj1, null);
System.out.println("Note类的类加载器是:" + clazz1.getClassLoader().getClass().getName());

System.out.println("====================================================================");

CustomizeClassLoader classLoader2 = new CustomizeClassLoader("D:/MyClasses-v2");
System.out.println("自定义类加载器的父加载器:" + classLoader2.getParent().getClass().getName());
Class clazz2 = classLoader2.loadClass("org.laugen.jvm.Note");
Object obj2 = clazz2.newInstance();
Method method2 = clazz2.getDeclaredMethod("print", null);
method2.invoke(obj2, null);
System.out.println("Note类的类加载器是:" + clazz2.getClassLoader().getClass().getName());
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V1版本)
创建了org.laugen.jvm.Note类的实例(V1版本)
这是一个note(V1版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader
====================================================================
自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V2版本)
创建了org.laugen.jvm.Note类的实例(V2版本)
这是一个note(V2版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader

由此可知,在同一个JVM中,两个相同全限定名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个时,出了要看类的全限定名外,还需要看他们的类加载器是不是同一个

那么,你知道Tomcat的JasperLoader热加载是怎么实现的吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security 实战干货:54版本带来的新

发表于 2021-03-24
  1. 前言

在以往Spring Security的教程中我们自定义配置都是声明一个配置类WebSecurityConfigurerAdapter,然后覆写(@Override)对应的几个方法就行了。然而这一切在Spring Security 5.4开始就得到了改变,从Spring Security 5.4 起我们不需要继承WebSecurityConfigurerAdapter就可以配置HttpSecurity 了。相关的说明原文:

  • Remove need for WebSecurityConfigurerAdapter #8805
  • Configure HTTP Security without extending WebSecurityConfigurerAdapter #8804

发稿时最新的Spring Security版本为 5.4.5。

  1. 新的配置方式

旧的配置方式目前依然有效:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
static class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
}

不过5.4.x版本我们有新的选择:

1
2
3
4
5
6
7
8
9
java复制代码@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.antMatcher("/**")
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.build();
}

这种JavaConfig的方式看起来更加清爽舒服,而且和适配器解耦了。等等我好像发现了新的东西,上面filterChain方法的参数是HttpSecurity类型。熟悉@Bean注解的同学应该会意识到一定有一个HttpSecurity类型的Spring Bean。没错!就在HttpSecurityConfiguration中有一个这样的Bean:

1
2
3
4
5
6
java复制代码@Bean({"org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity"})
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
// 省略掉
return http;
}

初始化的内容已经忽略掉,它不是本文关注的重点。我们注意到HttpSecurity被@Scope("prototype")标记。也就是这个HttpSecurity Bean不是单例的,每一次请求都会构造一个新的实例。这个设定非常方便我们构建多个互相没有太多关联的SecurityFilterChain,进而能在一个安全体系中构建相互隔离的安全策略。比如后端管理平台用Session模式,前台应用端用Token模式。

多个SecurityFilterChain

  1. 原理

Spring Security 有一个名为springSecurityFilterChain默认的过滤器链类(实际位置就是上图的 Bean Filter位置),其类型为FilterChainProxy, 它代理了所有的SecurityFilterChain,关键的代理注入代码:

1
2
3
4
5
6
7
8
9
java复制代码for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
break;
}
}
}

那么this.securityFilterChains来自哪里呢?

1
2
3
4
5
java复制代码@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
securityFilterChains.sort(AnnotationAwareOrderComparator.INSTANCE);
this.securityFilterChains = securityFilterChains;
}

到这里就一目了然了吧,SecurityFilterChain类型的Bean会被加载到this.securityFilterChains中。如果你的Spring Security 版本升级到 5.4.x**,就可以尝试一下这种方式。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点认证协议 普及篇之OAuth , OIDC , CA

发表于 2021-03-23

总文档 :文章目录

Github : github.com/black-ant

这一篇来聊一聊老本行 - 身份安全的相关概念 . 主要来说一说一般身份认证中常见的几种协议
这一篇主要说 OAuth , OIDC , CAS . 至于SAML , 我严重怀疑一篇说不完….

一 . 前言

我这里试着把协议分为几大类 :

  • 纯约束型协议 : OAuth , SAML , OIDC , CAS
  • 服务器类协议 : RADIUS , Kerberos , ADFS
  • 认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)
  • 认证服务器(附带) : AD , LDAP

这一篇我们只对流程进行一个普及 , 后面陆陆续续来分析一下其中的实现方式.

1.1 前置知识点 Token

Token 是认证过程中最常见的一个概念 , 它没有特定的规范 , 它仅仅是一个有着不同协议特征的钥匙 . 通常而言 , 他是有一定规律的无意义的字符串

1.2 前置知识点 JWT

JWT 全称 Json web token , JWT 通常由三部分组成 :

  • JWT 头 : 头部以 JSON 格式表示
    • alg : 签名使用的算法
    • typ : 令牌的类型 ,统一为 JWT
  • 有效载荷
    • iss:Issuer Identifier:必须。提供认证信息者的唯一标识
    • exp:Expiration time : 过期时间,超过此时间的ID Token会作废不再被验证通过
    • sub:Subject Identifier:主题。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户
    • aud:用户
    • nbf:在此之前不可用
    • iat: Issued At Time :JWT的构建的时间
    • jti:JWT ID用于标识该JWT
    • auth_time : AuthenticationTime : EU完成认证的时间
    • acr : Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类
    • amr : Authentication Methods References:可选。表示一组认证方法
    • azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用
  • 签名

二 . 纯约束型

纯约束型表示其长得就像个规范 ,而不同的框架去实现这个规范 (其实规范也叫框架 , 是一种狭义的框架)

2.1 OAuth 协议

2.1.1 OAuth 漫谈

通常我们说的 OAuth 是指其 2.0 版本 , 现在OAuth 已经公布了其 2.1 的版本 , 在结构上做了简化 , OAuth 其实不是一个完整的概念 , 它实际上是由许多不同的 rfc 组成的 (RFC 6749 , RFC 6750),它们以不同的方式相互构建并添加特性 , 就如下图所示 :

@ aaronparecki.com/2019/12/12/…
oauth-maze.png

在整个 OAuth 协议的发展中 , 他被陆陆续续添加了多个规范 , 并且实现了多种功能.

例如 RFC 6749 中就定义了我们用的最多的四种授权类型: 授权代码、隐式、密码和客户端凭据 , 而 RFC 7636 中则加入了 PKCE (一种无需客户机机密就可以使用授权代码流的方法) , RFC 8252 中将其建议为本机应用程序使用 .

直到陆陆续续到了 OAuth2.1 , 还剩下以下三种类型 :

  • Authorization code + PKCE
  • Client Credentials
  • Device Grant

这一篇不会涉及到过多的 OAuth2.1 , 主要是我的实现代码还没有写完…

OAuth2.0 支持的类型 :

  • 密码模式(resource owner password credentials)
  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 客户端模式(client credentials)
  • 新设备 (Device Grant) : 没有浏览器或者没有键盘的设备

OAuthType.png

2.1.2 OAuth 2.0 的流程 :

OAuth 的整个流程大概就长下面那样 :

  • Step 1 : 用一个方式去认证 , 认证成功后返回一个票据AccessToken (这个不限于一步完成)
  • Step 2 : 用返回的票据获取用户信息

OAuthCommon.jpg

OAuth2.0 的流程网络上已经说的太多了 ,自认为不会被前辈们画的更好 , 这里也就直接引用了:

总结性归类 :

这里细说一下三者的区别 :

Code 方式 :
Code 方式一般是企业最常用的一种方式 , 因为它很灵活 , 安全性也高 , 它和 implicit 以及 password 模式的区别主要是多了一个获取 Code 的过程 :

Step 1: Authoriza - > code : 发起请求返回code

Step 2: Code -> Token : 传递Code 换取Toekn

Step 3: Token -> UserInfo : 传递Token 换取用户信息

implicit 方式 :
implicit 方式是相对于 Code 的简化版 , 它由 Step1 Authoriza 直接来到了Token 步骤

password 方式 :
password 方式就变化较大了 ,它省去了跳转登录页认证的步骤 , 直接获取Token

OAuth Code 方式

OAuthCode.jpg

OAuth implicit 方式

OAuthImplict.jpg

OAuth Password 方式

OAuthPassword.jpg

OAuth 协议的接口请求一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码// Authoriza Code 模式
* 第一步 : http://localhost:8080/oauth/authorize?response_type=code&client_id=pair&redirect_uri=http://baidu.com

> Response_type -> 返回类型
> Client_id-> 对应的client id
> redirect_uri->重定向的地址

* 第二步 :
http://localhost:8080/oauth/token?grant_type=authorization_code&code=o4YrCS&client_id=pair&client_secret=secret&redirect_uri=http://baidu.com

> grant_type
> code
> client_id
> client_secret
> redirect_uri

* 第三步 :
通过Token 换取信息 ... 略

----------------------------
// implicit 模式 (略 , 第一步直接返回Code)


----------------------------
// Password 模式 (直接传入密码)
http://localhost:8080/oauth/accessToken?grant_type=password&client_id=b7a8cc2a-5dec-4a64&username=admin&password=123456

2.1.3 OAuth FAQ

问题一 : state 的作用
问题 : 当被攻击人(平民 A )登录时 ,让 A 认为登录的是自己的账号 ,但是 ,实际上 ,登录的是 攻击者 (狼人B)事先准备的账号 ,这就导致 A 在其上做的操作 ,B均可见 。

  • B 事先准备好攻击账号 ,进入第一步临时授权 ,获取到Code , 此时 ,基础验证已经完成 ,剩下访问 授权服务器 获取 Access Token.
  • B 此时强行停止 自身验证流程 ,骗取 A 进行点击 ,让 A 通过Code 完成后续登录 ,此时 A 以为登录的自身账号 ,并且 以为 正确进入系统

这就是通常说的中间人攻击 , 而有了 state , 开发者可以用这个参数验证请求有效性,也可以记录用户请求授权页前的位置 , 当然 ,也可以预防 CSRF

问题二 : implicit 和 Password 存在的场景 ?

Password 从请求上就可以看出有一定的安全漏洞 ,如果没有 SSL + 明文密码 , 这简直是把密码告诉别人了 , 而在我的使用中, 部分应用是发起存后台请求 , 不期望进行跳转 , 这个时候 , password 就能派上用场

问题三 : 待完善
TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207


2.2 OIDC 协议
-----------


讲了 OAuth 当然要来说一下 OIDC 这个小兄弟啦 , OIDC 其实很简单 , **就是在 OAuth 的基础上加入了 OpenID 的概念** , 你如果为了方便 , 复用 OAuth 的代码都没问题的. 即在 OAuth 的基础上额外携带一个 JWT 传递用户信息


#### 2.2.1 OIDC 简介


OpenID Connetction : **OIDC= (Identity, Authentication) + OAuth 2.0** , 它是一个基于 OAuth 2 的身份认证标准协议 , 通过 OAuth 2.0 构建了一个身份层 .


OIDC 提供了ID Token 来解决**第三方客户端标识用户身份** 的问题 ,在Oauth2 的授权流程中 ,一并提供用户的身份认证信息给第三方客户端 ,ID token 使用JWT 格式进行包装 **(得益于 JWT 的包容性 紧凑性 和 防篡改机制 ,并且提供 UserInfo ,可以回看一下开头的 JWT 扩展哦 )**



> OIDC 构成 信息



```
java复制代码- core : 定义 OIDC 的核心功能 ,在OAuth 2 之上 构建身份认证 ,以及使用 Claims 来传递用户信息
- Discovery : 发现服务 , 用于客户端动态的获取OIDC 服务相关的元数据描述信息
- Dynamic Registration : 动态注册服务 , 使客户端可以动态的注册到OIDC 的 OP
- OAuth 2.0 Multiple Response Types :可选。针对OAuth2的扩展,提供几个新的response_type。
- OAuth 2.0 Form Post Response Mode:可选。针对OAuth2的扩展,OAuth2回传信息给客户端是通过URL的querystring和fragment这两种方式,这个扩展标准提供了一基于form表单的形式把数据post给客户端的机制。
- Session Management :可选。Session管理,用于规范OIDC服务如何管理Session信息
- Front-Channel Logout:可选。基于前端的注销机制,使得RP(这个缩写后面会解释)可以不使用OP的iframe来退出
- Back-Channel Logout:可选。基于后端的注销机制,定义了RP和OP直接如何通信来完成注销

```

#### 2.2.2 OIDC 请求流程


1. RP 向 OP 申请 授权 ,OP 返回授权Access Token 以及 ID Token , 使用Access Token 向 User Info EndPoint 请求 信息
2. AuthN 请求虽然是复用OAuth 2 的 Authorization 请求 ,但是用途不一样 ,OIDC 的 authN scope 参数 必须要有一个openid 的参数



```
java复制代码The RP (Client) sends a request to the OpenID Provider (OP).
The OP authenticates the End-User and obtains authorization.
The OP responds with an ID Token and usually an Access Token.
The RP can send a request with the Access Token to the UserInfo Endpoint.
The UserInfo Endpoint returns Claims about the End-User.

+--------+ +--------+
| | | |
| |---------(1) AuthN Request-------->| |
| | | |
| | +--------+ | |
| | | | | |
| | | End- |<--(2) AuthN & AuthZ-->| |
| | | User | | |
| RP | | | | OP |
| | +--------+ | |
| | | |
| |<--------(3) AuthN Response--------| |
| | | |
| |---------(4) UserInfo Request----->| |
| | | |
| |<--------(5) UserInfo Response-----| |
| | | |
+--------+ +--------+

// 详细步骤 :
》 OIDC 单点登录流程
> 1 . 用户点击登录 ,触发对OIDC-SERVER 的认证请求
|-> request : 包含参数URL , 指向登录成功后的跳转地址
|-> response : 返回 302 ,Location 指向 OIDC-SERVER ,Set-Cookie 设置了 nonce的cookie
> 2 . 向 OIDC-SERVER 发起 authc 请求
|-> client_id=implicit-client :发起认证请求的客户端的唯一标识
|-> reponse_mode=form_post :使用form表单的形式返回数据
|-> response_type=id_token :返回包含类型 id_token
|-> scope=openid profile :返回包含有openid这一项
|-> state :等同于OAuth2 state ,用于保证客户端一致性
|-> nonce : 写入的cookie 值
|-> redirect_uri : 认证成功后的回调地址
> 3 . OIDC-SERVER 验证 authc 请求
|-> client_id是否有效,redircet_uri是否合法 等一系列验证
> 4 . 引导用户登录 ,以及用户登录
|-> resumeURL
|-> username + password
> 5 . 返回一个自动提交form 表单的页面
|-> id_token:id_token即为认证的信息,OIDC的核心部分,采用JWT格式包装的一个字符串
|-> scope:用户允许访问的scope信息
|-> state : 类似
|-> session_state :会话状态
> 6 . 验证数据有效性,构造自身登录状态
|-> 客户端验证id_token的有效性 ,保证客户端得到的id_token是oidc-sercer.dev颁发的


```


> OIDC 接口演示 ------------> 真不记得是哪位大佬的案例了....



```
java复制代码
// OIDC 直观流程 请求地址 :
// Step 1 : 发起 Authorize 请求
https://${yourDomain}/oauth2/default/v1/authorize?response_type=code
&client_id=12345
&redirect_uri=https://proxy.example.com:3080/v1/webapi/oidc/callback
&scope=openid,email
&state=syl

// Step 2 : 认证成功后重定向返回
https://proxy.example.com:3080/v1/webapi/oidc/callback?code=pkzdZumQi1&state=syl

// Step 3 : 申请 Token
POST https://${yourOktaDomain}/oauth2/default/v1/token
?grant_type=authorization_code&
code=pkzdZumQi1&
redirect_uri=https://proxy.example.com:3080/v1/webapi/oidc/callback
client_id=12345&
client_secret=gravitational

// Step 4 : AccessToken 返回
{
"id_token":"FW6AlBeyalZtDIRXxA0u5XBbZkLzjYzKUQBloxQXSSGPFmRS8eSfDu0A4nS4GF1aQP9PRxQk7gIh9bjaX99
aa4vDSzP1E2ajsgIomlNGhNxBqEDV5Exp0xISE9bZ4HUzM91pbzPPj7Bq5ZQUWcSuSVD0NAfkAoG6qDpbQfxPjWRyfthz3p
UEXwZe8Cz4eOXOM45UKB4Q0VnVSChVF84MWkeBFKzhrRNXd2dFv0HTlkQr6vXGlYsocMxR06wo38HvGiKjkUmL2YUyPOjZa
oUN4ovfwlwdGdjNR2GVcRsXzjxCPszJ9dTXztoL5wo2ycEpuxkkNp57BuZ9YRexoNnRHahFKH76XrFsTvdvAYk3fBVUqrO5
vvyxHAFrAIKpV0FvaMiBwKNfaE84oRC6aBXnzS3q4uVyGcHveHQMJB1temgB599rfVH3pBqurUmQCd0tVexRZj4PUkrDocf
8Z0QKkCD0eonH0Q1bRpQPY5vATiLkpF8RArU7wyB2FxhB3egtQBvwDgsVjyix7u8Cx4P9oy3IJje6SZfc6Lz61uEQttpVhy
qfzgFYUqVoQacw6rocCn3u61dM0moB"
"access_token":"IEZKr6ePPtxZBEd",
"token_type":"bearer",
"scope":"read:org",
"expires_in":3600
}

// Step 5 : 对 id_token 进行解码
{
"sub":"virag",
"iss":"https://${yourOktaDomain}/oauth2/",
"aud":"client_12345",
"iat":"1595977376",
"exp":"1595980976",
"email":"virag@goteleport.com",
"email_verified":"true"
}

// Step 6 : 对资源进行请求


```

#### 2.2.3 OIDC 扩展文档



```
java复制代码https://www.ubisecure.com/education/differences-between-saml-oauth-openid-connect/

https://www.okta.com/identity-101/whats-the-difference-between-oauth-openid-connect-and-saml/

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols

https://yangsa.azurewebsites.net/index.php/2019/08/08/brief-summary-of-differences-between-oauth2-and-oidc/

https://www.c-sharpcorner.com/article/oauth2-0-and-openid-connect-oidc-core-concepts-what-why-how/

```

#### 2.2.4 OIDC 的额外知识点



> OIDC Discovery 规范


定义了一个服务发现的规范,它定义了一个api( /.well-known/openid-configuration ),这个api返回一个json数据结构,其中包含了一些OIDC中提供的服务以及其支持情况的描述信息,这样可以使得oidc服务的RP可以不再硬编码OIDC服务接口信息



> 会话管理


* Session Management :可选。Session管理,用于规范OIDC服务如何管理Session信息。
* Front-Channel Logout:可选。基于前端的注销机制。
* Back-Channel Logout:可选。基于后端的注销机制。



> OIDC 的好处


* OIDC使得身份认证可以作为一个服务存在。
* OIDC可以很方便的实现SSO(跨顶级域)。
* OIDC兼容OAuth2,可以使用Access Token控制受保护的API资源。
* OIDC可以兼容众多的IDP作为OIDC的OP来使用。
* OIDC的一些敏感接口均强制要求TLS,除此之外,得益于JWT,JWS,JWE家族的安全机制,使得一些敏感信息可以进行数字签名、加密和验证,进一步确保整个认证过程中的安全保障。


2.3 CAS
-------


CAS 我可太熟了 , 这还不随便和大家扯淡

2.3.1 CAS 术语

CAS分为两部分,CAS Server和CAS Client

  • CAS Server用来负责用户的认证工作,就像是把第一次登录用户的一个标识存在这里
  • CAS Client就是我们自己开发的应用程序,需要接入CAS Server端

CAS 的三个术语

  • Ticket Granting ticket (TGT) :可以认为是CAS Server根据用户名密码生成的一张票,存在Server端
    • 缓存后会配合TGC 生成ST (因为TGT 可以标识用户已经登陆过)
  • Ticket-granting cookie (TGC) :其实就是一个Cookie,存放用户身份信息,由Server发给Client端
  • Service ticket (ST) :由TGT生成的一次性票据,用于验证,只能用一次

2.3.2 CAS 处理流程

  • 用户第一次访问网站,重定向到CAS Client , 发现没有cookie(TGC或者没有ST) ,重定向到 CAS Server端的登录页面
  • 在登陆页面输入用户名密码认证(Web 和 Server 交互),认证成功后cas-server生成TGT,再用TGT生成一个ST
  • 然后再第三次重定向并返回ST和cookie(TGC)到浏览器
  • 浏览器带着ST再访问想要访问的地址
  • 浏览器的服务器收到ST后再去cas-server验证一下是否为自己签发的
  • 再登陆另一个接入CAS的网站,重定向到CAS Server,server判断是第一次来(但是此时有TGC,也就是cookie,所以不用去登陆页面了)
  • 但此时没有ST,去cas-server申请一个于是重定向到cas-server
  • cas-server 通过TGT + TGC 生成了ST
  • 浏览器的服务器收到ST后再去cas-server验证一下是否为自己签发的

我知道一般人懒得看 ,我也是 ,所以我画了一张图!!!

cas.jpg

2.3.3 CAS FAQ

CAS 与 OAuth 最大的几个区别 :

CAS 和 OAuth 都是一种认证结构/协议 , 而 Token/ST则是属于一种票据的方式 , 并没有特定的归属

  • 1 资源和用户信息 :
    • 首先要明确这两者不是同一种东西 !
    • 资源是受保护的 , 是认证成功后才可以访问的 , 通常情况下 , CAS 的资源在客户端 , OAuth 方式的资源存在于服务端
    • 用户信息在认证系统里面是登录时 ,和认证成功后返回的 , 其显式或者隐式存在于服务端
  • 2 申请流程的细微区别:
    • CAS 完成二次认证时 (已经登陆过), 可以直接拿着ST 去进行认证 , 这样做的原因一个是因为其认session 和 Token 中
    • OAuth 进行认证的时候 , 除了需要一个临时码(Code) , 另外需要一个ClientSecret , 用来判断客户端是否合法
  • 3 客户端 :
    • 基于上一点 , CAS 看来 , 客户端基本上是一致的 , 不论何种 ,将TGC 放入Cookie 就行
    • OAuth 的客户端虽然业务上是同一类东西 , 但是人为的对客户端进行了身份处理

CAS 认证的票据 :

CAS 通过将 TGC 写入 Cookie , 当下次认证是从 Cookie 中取出 TGC 认证 ,所以要想做跨浏览器登录 , 可以在这里做文章哦 !

总结

今天先这样了 , 又要到转钟了 , 这篇文章既是盘点 , 也是对自身知识图谱的完善 , 认证协议多种多样 , 常规的应用通常只需要选择其中最合适的一种去实现自己的业务即可.

参考与感谢

讲道理 , 挺多的 ,但是记得时候又没有把地址记下来 , 懒得找了 , 直接祝大家身体健康吧

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Deprecated】算法 100000 个数的求和只

发表于 2021-03-23

提示:这篇文章是 21 年写的,今年对此进行重写并增加了更多新思考:使用前缀和数组解决”区间和查询”问题

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。


前言

  • 前缀和是一种非常适合处理 区间查询 问题的算法技巧,理解前缀和的思想对后续学习 线段树、字典树 很有帮助;
  • 在这篇文章里,我将梳理前缀和的基本知识 & 常考题型。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


  1. 前缀和 + 差分

首先,我们使用一道典型例题来引入前缀和的概念。

303. 区域和检索 - 数组不可变 【题解】

给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

这道题的暴力解法是很容易想到的,区间[i,j][i, j][i,j]的和无非就是累加区间元素即可,时间复杂度是O(n)O(n)O(n),空间复杂度是O(1)O(1)O(1)。但如果多次检索区间[i,j][i, j][i,j]的和,暴力解法就显得不优雅了,因为暴力解法中存在 很多重复求和运算。举个例子,暴力解法计算区间[1,5][1, 5][1,5]和区间[1,10][1, 10][1,10],区间[1,5][1, 5][1,5]的元素就重复执行了求和运算。

那么,有没有办法优化区间和的时间复杂度呢?在 O(1)O(1)O(1) 时间复杂度内计算[5000,1000000][5000,1000000][5000,1000000]的区间和,有可能吗?方法总是有的,无非是利用空间换取时间,这就需要使用「前缀和 + 差分」技巧了。

前缀和的基本套路是开辟一个前缀和数组,存储「元素所有前驱节点的和」,例如:

1
2
3
4
5
scss复制代码val sum = IntArray(nums.size + 1) { 0 }

for (index in nums.indices) {
sum[index + 1] = sum[index] + nums[index]
}

利用前缀和数组,可以很快计算出区间 [i, j] 的和:nums[i,j]=preSum[j+1]−preSum[i]nums[i, j] = preSum[j + 1] - preSum[i]nums[i,j]=preSum[j+1]−preSum[i]

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class NumArray(nums: IntArray) {
private val sum = IntArray(nums.size + 1) { 0 }

init {
for (index in nums.indices) {
sum[index + 1] = sum[index] + nums[index]
}
}

fun sumRange(i: Int, j: Int): Int {
return sum[j + 1] - sum[i] // 注意加一
}
}

提示: 前缀和数组长度不一定要比原数组长度大 1,加 1 的目的是为了在后面做差分的时候代码会比较简洁。

另外,前缀和还适用于二维区间和检索,思路都是类似的,你可以试试看:

304. 二维区域和检索 - 矩阵不可变 【题解】

  1. 前缀和 + 哈希表优化

这一节我们来讨论前缀和结合哈希表的优化技巧,这种技巧一般是适用在一些 只关心次数,不关心具体的解 的场景。利用哈希表 O(1)O(1)O(1) 时间复杂度查询的特性,可以进一步优化时间复杂度。

同样地,我们使用一道典型例题来展开讨论:

560. 和为K的子数组 【题解】

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

题目的要求是计算和为 k 的子数组个数,掌握了使用前缀和计算区间和后,我们可以轻松地写出一种解法:在这里我们枚举每个子数组,使用「前缀和 + 差分」技巧计算区间和为 kkk 的个数:

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码class Solution {
fun subarraySum(nums: IntArray, k: Int): Int {
1、预处理:构造前缀和数组
var preSum = IntArray(nums.size + 1) { 0 }
for (index in nums.indices) {
preSum[index + 1] = preSum[index] + nums[index]
}

2、枚举所有子数组,使用「前缀和 + 差分」技巧计算区间和
var result = 0
for (i in nums.indices) {
for (j in i until nums.size) {
val sum_i_j = preSum[j + 1] - preSum[i]
if (k == sum_i_j) {
result++
}
}
}
return result
}
}

整个算法的时间复杂度是O(n2)O(n^2)O(n2),空间复杂度是O(n)O(n)O(n),这是最优算法了吗?因为题目要求的是数组个数,而不关心具体的数组,所以我们大可不必枚举全部子数组(一共有n2n^2n2个子数组),我们只需要在计算出当前位置的前缀和之后,观察之前位置符合条件的前缀和个数即可。

具体来说,对于当前位置已经计算出前缀和 preSum[j]preSum[j]preSum[j],我们只需要寻找在[0,j−1][0, j -1][0,j−1]区间内,前缀和为preSum[j]−kpreSum[j] - kpreSum[j]−k 的个数。

紧接着的问题是:怎么快速查找前缀和为preSum[j]−kpreSum[j] - kpreSum[j]−k 的个数呢?聪明的你一定知道是哈希表了,我们需要维护一个哈希表:Key 为前缀和,Value 为前缀和出现次数。

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kotlin复制代码class Solution {
fun subarraySum(nums: IntArray, k: Int): Int {
var preSum = 0
var result = 0

维护一个哈希表:Key 为前缀和,Value 为前缀和出现次数
val map = HashMap<Int, Int>()
在索引 0 之前,存在一个前缀和为 0 的空数组
map[0] = 1

for (index in nums.indices) {
preSum += nums[index]

查询前缀和为 preSum - k 的个数
val offset = preSum - k
if (map.contains(offset)) {
result += map[offset]!!
}

map[preSum] = map.getOrDefault(preSum, 0) + 1
}

return result
}
}

如此,整个算法的时间复杂度降低为O(n)O(n)O(n),空间复杂度保持为O(n)O(n)O(n)。


  1. 举一反三

525. 连续数组 【题解】
1004. 最大连续1的个数 III
1248. 统计「优美子数组」
974. 和可被 K 整除的子数组
352. 和等于 K 的最长子数组长度
1314. 矩阵区域和

  1. 总结

  • 前缀和技巧适用于区间查询等问题,当问题中提到 “连续子数组” 时,可以考虑使用「前缀和 + 差分」技巧;
  • 在只关心次数,不关心具体的解的场景,可以使用前缀和 + 哈希表进一步优化时间复杂度;
  • 前面提到的问题都属于静态数据场景,也就是前缀和只需要计算一次即可。如果数据是动态的呢,还可以使用前缀和吗?你可以思考下这两道题。
307. 区域和检索 - 数组可修改
308. 二维区域和检索 - 矩阵不可变

参考资料

  • 《前缀和技巧》 —— labuladong 著
  • 《650. 题解》 —— liweiwei1419 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分库分表实战及中间件(一) 分库分表实战及中间件 背景描述

发表于 2021-03-23

分库分表实战及中间件

背景描述

在项目中,使用单库单个mysql去存储数据,其中我们某个表的数据量目前是3000w 、某个表由于客户一些创建的数据几乎每天增量数据是几十万,而且每个客户相对对应的数据增量也不仅相同。考虑到之后到某个节点时间数据可能会达到上限1个亿。
我们都知道mysql单表数据量是3000w-5000w左右,如果增量再多,单表的查询效率会变慢。

对此我们需要将其分开存储,也就是需要考虑分库分表的时候了。那我们如何进行分库分表操作呢?

为什么要分表

上面也说了,由于单表的数据量在未来会达到一个亿级别甚至更高,单表查询效率会变慢。

这儿是单表查询数据量会变大。故此我们采用分表操作,业务并不需要分库操作。

即单表数据量太大

在之后的查询、插入、更新操作都会变慢,在加字段、加索引、机器迁移都会产生高负载,影响服务。

对于分表我们怎么选择?

分表分为水平分表和垂直分表

水平分表

针对数据量巨大的单张表(比如订单表),按照规则把一张表的数据切分到多张表里面去。但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。

缺点是:并没有解决单个数据库并发以及IO的提升,如需要提升可增加机器配置,或者进行水平分库又分表。
好处就不用多说了。

水平分表规则
  • RANGE

时间:按照年、月、日去切分。例如order_2020、order_202005、order_20200501

地域:按照省或市去切分。例如order_beijing、order_shanghai、order_chengdu

大小:从0到1000000一个表。例如1000001-2000000放一个表,每100万放一个表HASH

用户ID取模不同的业务使用的切分规则是不一样,就上面提到的切分规则,举例如下:

站内信

用户维度:用户只能看到发送给自己的消息,其他用户是不可见的,这种情况下是按照用户ID hash分库,在用户查看历史记录翻页查询时,所有的查询请求都在同一个库内用户表

范围法:以用户ID为划分依据,将数据水平切分到两个数据库实例,如:1到1000W在一张表,1000W到2000W在一张表,这种情况会出现单表的负载较高

按照用户ID HASH尽量保证用户数据均衡分到数据库中如果在登录场景下,用户输入手机号和验证码进行登录,这种情况下,登录时是
不是需要扫描所有分库的信息?

最终方案:用户信息采用ID做切分处理,同时存储用户ID和手机号的映射的关系表(新增一个关系表),关系表采用手机号进行切分。可以通过关系表根据手机号查询到对应的ID,再定位用户信息。

流水表

时间维度:可以根据每天新增的流水来判断,选择按照年份分库,还是按照月份分库,甚至也可以按照日期分库

垂直分表

表中字段太多且包含大字段的时候,在查询时对数据库的IO、内存会受到影响,同时更新数据时,产生的binlog文件会很大,MySQL在主从同步时也会有延迟的风险。

大白话即一个表中有很多字段假如50个字段,平时我们经常使用的有5-10个字段,对于其他字段万年几乎使用一次,对此我们可以通过主键关联将其拆到另一张表中。这就是分表可以理解为类似于我们将单体服务拆分成多个微服务。

缺点是:如果拆分不好的话,多出一张表,对于其他业务,需要关联使用,增加额外表关联操作。
好处就不用多说了。

对于目前这儿的业务而言,目前采用分表操作。

分析

根据我们目前的业务(具体是做什么的,由于工作的原因,目前无法透露),由于我们的数据量对于订单表和某个表以下都简称C表,尤其是C表数据量在未来可遇见的情况下,数量会达到一个量。

所以这儿我们采用了对此C表进行分表操作,简而言之根据每个客户对应一个表进行分表操作。

简单的来说,A,B,C,D,E,F….M客户,分表之前产生的数据都在C表中。
在我们分表之后A,B,C,D,E,F等表分表对应C1,C2,C3,C4等表。

理论上来说可以每个客户对应一个表C,但实际的情况是,每个客户产生的数据不是相同的,有的客户一天甚至几w数据,有的客户甚至以后个月才1W数据。

故此,在分表之前需要对客户数据进行分析,可以通过一些sql分析查询,假设统计分析之后数据如下

客户 数据量 /月
A 9W
B 10W
C 11W
D 0.2W
E 5W
F 7W
H 0.8W
I 2W

对于ABCE等客户可以每个对应一个表C1,C2,C3,C4对于EHI我们可以让其对应表C5 , 其实对于F也可以对应一张表(可以根据自己的业务进行调整)

也就是如下图

在这里插入图片描述

实现

以上是简单的对业务分表的分析,然后才能对齐进行分表。

我们使用了客户(这儿根据客户id)进行分表,然后我们开始选择技术栈,对于市面上的分表中间件而言,有MyCat 和Sharding-JDBC
首先我们不使用中间件而言如何进行分表操作呢。

对于程序而言,在查询的时候根据不同的id去不同的C表(C1,C2,C3,C4等)中进行查询,新增更新删除也如此。
就需要我们手动的去指定响应的路由规则即根据id去找到相应的表C;

对此手动路由可以看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码   Map<Integer,String> map =  new HashMap<>();
map.put(1,"C1");
map.put(2,"C2");
map.put(3,"C3");
map.put(4,"C4");
map.put(5,"C5");
map.put(6,"C6");
map.put(7,"C6");


//获取表C
map.get(1);

在代码中进行JDBC操作时候,我们手动的获取响应的表,然后通过替换符在sql中将C表进行替换即可。基本不影响sql操作包括join连表。

1
2
3
4
5
xml复制代码 SELECT
s.a,
s.b,
FROM
${tableName} ss

但对于查询所有客户数据而言,因为分表的缘故,所有的数据都不在一个表中。对此我们可能需要将其数据都查询出来,然后将其组合起来。在sql或者java代码中都可以操作。

在Sql中,可以通过union关键字将其所有结果都关联起来查询返回,在业务代码中可以通过for循环等操作将其查询组合起来。

这样也存在缺点:

sql中sql长度变长,编译sql时间长,全部查询需要更改sql
业务代码中需要编写代码,需要组合查询,额外查询次数变多(单表中查询需要一次),这儿也可以通过保存一份所有的id数据表C。

最后

在上线前需要迁移数据,将不同id对应的数据迁移到不同的C表中。

总结

在代码中手动去路由分表

优点:简单,不需要引入其他中间件,复杂sql也能自己去实现(sharding-jdbc某些sql不支持)。

缺点也是显而易见的,代码改动大,成本大,几乎所有需要用到的C表都需要手动根据路由去获取C表,然后替换。后续业务发生变化,还需要改动代码。

分表优缺点

优点:单表数据分散存储在不同的表中,提高查询速度。
缺点:没有解决单个数据的IO以及连接数以及并发。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

互联网公司的技术人,为什么不写文档?

发表于 2021-03-23

‍互联网公司,技术侧,写文档有没有必要?\

有必要。\

要写什么文档?

至少要写总体设计文档,详细设计文档。\

为什么不写?

可能是没时间,可能是不会写,可能是不愿意写。

本文试图分享一些经验,解决“不会写”的问题。\

**总体设计文档,详细设计文档,应该包含什么内容?

**总设和详设都应该包含的部分:****

(1) 需求:一般以产品的语言描述,这一块可以拷贝产品需求文档中的story list部分;

(2) 名词解释(可选):非相关领域内的同学需要看到文档需要提前了解的一些概念性质的东西;

(3) 设计目标:又分为功能目标和性能目标,功能目标一般是对产品需求的技术描述,性能目标是根据产品给出的数据对性能进行的评估。一般来说,新服务必须要有性能目标一项,性能目标可能会影响设计方案。

\

除了都应该包含的部分,总体设计一般还包含:

(1) 系统架构:一般来说会有个简单的架构图,并配以文字对架构进行简要说明;

(2) 模块简介:架构图中如果有很多模块,需要对各个模块的功能进行简要介绍;

(3) 设计与折衷:设计与折衷是总体设计中最重要的部分;

(4) 潜在风险(可选);

输出总体设计的时候,很多方案还是不确定的,故总体设计重点在“方案折衷”,方案需要在设计评审会议上确认。

总体设计评审完毕之后,此时应该是所有方案都确认了,需要输出各模块的详细设计。

详细设计重点在“详细”,需要包含:

(1) 总体设计结论汇总(可选):总体设计上达成一致的结论有个简要概述,说明详设是对这些结论的实现;

(2) 交互流程:简要的交互可用文字说明,复杂的交互建议使用流程图,交互图或其他图形进行说明;

(3) 数据库设计:这个是应该放在总设还是详设呢?

(4) 接口细节:输入什么参数,输出什么参数,根据接口前端、后端、APP、QA就能够并行做编码实现了;

(5) 其他细节:例如公式等;

理论上输出了详细设计之后,无论谁拿到了这个详设文档,都是能够完成该项目的。

其他最佳实践?

一、 大图

(1) 大系统或复杂流程,其架构图或者流程图会非常大,经常比A4纸或word的一页大很多,此时不宜在word中直接贴图形,贴了也看不清,建议将图放在wiki上,文档中直接贴链接;

(2) 一定要保存viso或者其他图形的源文件,否则今后改动起来要重画,代价可想而知;

\

二、 设计与折衷

(1) 设计与折衷是总设中最重要的内容,总设评审中,主要就是讨论这些折衷的优劣;

(2) 评审过后,不但要邮件周知结论,还要在总设中进行更新,说明最终决定使用了哪种方案,为什么使用这种方案;根据自己的经验,接手别人的模块、项目,拿到代码和文档,设计方案对我来说完全是个谜!!!

(3) 有时候因为排期或者其他原因,不一定采用了最优的设计方案,此时更应该在总设中记录决策的过程与原因;

(4) 最后,设计折衷是一个很好的自我辩解的机会:因为项目进度,或者历史遗留问题,我不得不采取了一个这样的设计,不要再骂我了。

\

三、 性能目标

性能目标是新模块文档必不可少的一部分,很多项目对性能影响较大的话,也必须撰写性能目标,性能一般来说可能包含以下部分:

(1) 日平均请求:一般来自产品人员的评估;

(2) 平均QPS:日平均请求 除以 4w秒得出,为什么是4w秒呢,24小时化为86400秒,取用户活跃时间为白天算,除2得4w秒;

(3) 峰值QPS:一般可以以QPS的2~4倍计算;

画外音: 详见《 架构,如何进行容量设计? 》。

互联网公司,产品迭代快,可能很多公司没有“文档”一说。但其实,写好文档,对系统和项目未来的维护是非常有帮助的。

画外音: 文档清楚,开发阶段变化小; 未来迭代成本小。

**架构师之路-分享可落地技术\**\

相关推荐:\

你们公司写设计文档么?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

放弃FastJson!一篇就够,Jackson的功能原来如此

发表于 2021-03-23

在上篇《经过多方调研,最终还是决定禁用FastJson!》中,讲了FastJson的基本使用以及存在的不确定性问题,所以最终决定在项目中放弃使用,进而选择市面上比较主流,Spring Boot默认绑定的JSON类库:Jackson。

本文就来讲解一下Jackson的基本使用以及与Spring Boot的结合与实践。

什么是Jackson

Jackson是比较主流的基于Java的JSON类库,可用于Json和XML与JavaBean之间的序列化和反序列化。

没看错,Jackson也可以处理JavaBean与XML之间的转换,基于jackson-dataformat-xml组件,而且比较JDK自带XML实现更加高效和安全。而我们使用比较多的是处理JSON与JavaBean之间的功能。

Jackson主流到什么程度?单从Maven仓库中的统计来看,Jackson的使用量排位第一。而Spring Boot支持的三个JSON库(Gson、Jackson、JSON-B)中,Jackson是首选默认库。

Jackson也有以下特点:依赖少,简单易用,解析大Json速度快、内存占用比较低、拥有灵活的API、方便扩展与定制。

Jackson类库GitHub地址:github.com/FasterXML/j… 。

Jackson的组成部分

Jackson的核心模块由三部分组成(从Jackson 2.x开始):jackson-core、jackson-annotations、jackson-databind。

  • jackson-core:核心包,定义了低级流(Streaming)API,提供基于”流模式”解析。Jackson内部实现正是通过高性能的流模式API的JsonGenerator和JsonParser来生成和解析json。
  • jackson-annotations,注解(Annotations)包,提供标准的Jackson注解功能;
  • jackson-databind:数据绑定(Databind)包,实现了数据绑定(和对象序列化)支持,它依赖于Streaming和Annotations包。提供基于“对象绑定”解析的API(ObjectMapper)和”树模型”解析的API(JsonNode);基于”对象绑定”解析的API和”树模型”解析的API依赖基于“流模式”解析的API。

下面看一下不同环境下相关组件的依赖引入情况。

在SpringBoot当中,spring-boot-starter-web间接引入了Jackson组件,也就是如果你使用了SpringBoot框架,那么你的项目中已经有了Jackson依赖。下面的依赖省略了version和scope项。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

web starter中依赖了json starter:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>

json starter最终引入了Jackson:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>

上面已经提到过,jackson-databind依赖于Streaming和Annotations包,因此,引入jackson-databind相当于引入了jackson-core和jackson-annotations。

通常情况下,我们单独使用时,根据需要通过Maven引入jackson-databind、jackson-core和jackson-annotations即可。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

对于SpringBoot项目,基本上不用再额外添加依赖。

Jackson核心类ObjectMapper

Jackson提供了三种JSON的处理方式,分别是:数据绑定、JSON树模型、流式API。其中前两项功能都是基于ObjectMapper来实现的,而流式API功能则需要基于更底层的JsonGenerator和JsonParser来实现。

通常情况下我们使用ObjectMapper类就足够了,它拥有以下功能:

  • 从字符串、流或文件中解析JSON,并创建表示已解析的JSON的Java对象(反序列化)。
  • 将Java对象构建成JSON字符串(序列化)。
  • 将JSON解析为自定义类的对象,也可以解析JSON树模型的对象;

ObjectMapper基于JsonParser和JsonGenerator来实现JSON实际的读/写。这一点看一下ObjectMapper的构造方法即可明白。

具体实例

Jackson的常见使用,就不逐一讲解了,通过一些列的实例给大家展示一下,每个实例当中都会通过注释进行说明。

常见简单使用

下面的示例是我们经常会用到的用法演示,主要涉及到JavaBean和Json字符串之间的转换。

Jackson在将json转换为JavaBean属性时,默认是通过Json字段的名称与Java对象中的getter和setter方法进行匹配进行绑定。

Jackson取getter和setter方法名称中去除“get”和“set”部分,并将首字母小写。例如Json中的name,与JavaBean中的getName()和setName()进行匹配。

但并不是所有的属性都可以被序列化和反序列化,基本上遵循一下规则:

  • public修饰的属性可序列化和反序列化。
  • 属性提供public的getter/setter方法,该属性可序列化和反序列化。
  • 属性只有public的setter方法,而无public的getter方法,该属性只能用于反序列化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
swift复制代码@Slf4j
public class JacksonTest {

/**
* JavaBean转JSON字符串
*/
@Test
public void testJavaBeanToJson() {
WeChat weChat = new WeChat();
weChat.setId("zhuan2quan");
weChat.setName("程序新视界");
weChat.setInterest(new String[]{"Java", "Spring Boot", "JVM"});

ObjectMapper mapper = new ObjectMapper();
try {
String result = mapper.writeValueAsString(weChat);
System.out.println(result);
} catch (JsonProcessingException e) {
log.error("转换异常", e);
}
}

/**
* JSON字符串转JavaBean
*/
@Test
public void testJsonToJavaBean() {
String json = "{\"id\":\"zhuan2quan\",\"name\":\"程序新视界\",\"interest\":[\"Java\",\"Spring Boot\",\"JVM\"]}";
ObjectMapper mapper = new ObjectMapper();
try {
WeChat weChat = mapper.readValue(json, WeChat.class);
System.out.println(weChat);
} catch (JsonProcessingException e) {
log.error("解析异常", e);
}
}

/**
* JSON字符串转Map集合
*/
@Test
public void testJsonToMap() {
String json = "{\"id\":\"zhuan2quan\",\"name\":\"程序新视界\",\"interest\":[\"Java\",\"Spring Boot\",\"JVM\"]}";
ObjectMapper mapper = new ObjectMapper();
try {
// 对泛型的反序列化,使用TypeReference可以明确的指定反序列化的类型。
Map<String, Object> map = mapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
System.out.println(map);
} catch (JsonProcessingException e) {
log.error("解析异常", e);
}
}

/**
* JavaBean转文件
*/
@Test
public void testJavaBeanToFile() {
WeChat weChat = new WeChat();
weChat.setId("zhuan2quan");
weChat.setName("程序新视界");
weChat.setInterest(new String[]{"Java", "Spring Boot", "JVM"});

ObjectMapper mapper = new ObjectMapper();
try {
//写到文件
mapper.writeValue(new File("/json.txt"), weChat);

//从文件中读取
WeChat weChat1 = mapper.readValue(new File("/json.txt"), WeChat.class);
System.out.println(weChat1);
} catch (IOException e) {
log.error("转换异常", e);
}
}

/**
* JavaBean转字节流
*/
@Test
public void testJavaBeanToBytes() {
WeChat weChat = new WeChat();
weChat.setId("zhuan2quan");
weChat.setName("程序新视界");
weChat.setInterest(new String[]{"Java", "Spring Boot", "JVM"});

ObjectMapper mapper = new ObjectMapper();
try {
// 写为字节流
byte[] bytes = mapper.writeValueAsBytes(weChat);
// 从字节流读取
WeChat weChat1 = mapper.readValue(bytes, WeChat.class);
System.out.println(weChat1);
} catch (IOException e) {
log.error("转换异常", e);
}
}
}

上述代码用到了lombok注解和单元测试注解,根据需要可进行替换。

JSON树模型

如果Json字符串比较大,则可使用JSON树模型来灵活的获取所需的字段内容。在Jackson中提供了get、path、has等方法来获取或判断。

下面直接看两个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
swift复制代码@Slf4j
public class JacksonNodeTest {

/**
* JavaBean转JSON字符串
*/
@Test
public void testJsonNode() {
// 构建JSON树
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
root.put("id", "zhuan2quan");
root.put("name", "程序新视界");
ArrayNode interest = root.putArray("interest");
interest.add("Java");
interest.add("Spring Boot");
interest.add("JVM");

// JSON树转JSON字符串
String json = null;
try {
json = mapper.writeValueAsString(root);
} catch (JsonProcessingException e) {
log.error("Json Node转换异常", e);
}
System.out.println(json);
}

/**
* 解析JSON字符串为JSON树模型
*/
@Test
public void testJsonToJsonNode() {
String json = "{\"id\":\"zhuan2quan\",\"name\":\"程序新视界\",\"interest\":[\"Java\",\"Spring Boot\",\"JVM\"]}";
ObjectMapper mapper = new ObjectMapper();
try {
// 将JSON字符串转为JSON树
JsonNode jsonNode = mapper.readTree(json);
String name = jsonNode.path("name").asText();
System.out.println(name);

JsonNode interestNode = jsonNode.get("interest");
if (interestNode.isArray()){
for (JsonNode node : interestNode){
System.out.println(node.asText());
}
}
} catch (JsonProcessingException e) {
log.error("Json Node转换异常", e);
}
}
}

其中get方法和path功能相似,区别在于如果要读取的key在Json串中不存在时,get方法会null,而path会返回MissingNode实例对象,在链路方法情况下保证不会抛出异常。

流式API

除了上述两种形式,还可以基于底层的流式API来进行操作,主要通过JsonGenerator和JsonParser两个API,但操作起来比较复杂,就不再这里演示了。

格式化统一配置

在使用ObjectMapper时,会存在一些字段在某些情况下不需要进行序列化或反序列化,同时还可能需要指定格式化的一些信息等。此时,可以通过ObjectMapper进行配置。

1
2
3
4
5
6
7
8
9
10
scss复制代码//反序列化时忽略json中存在但Java对象不存在的属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//序列化时日期格式默认为yyyy-MM-dd'T'HH:mm:ss.SSSZ
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//序列化时自定义时间日期格式
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
//序列化时忽略值为null的属性
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//序列化时忽略值为默认值的属性
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);

针对于配置项,在2.2版本中新增了一个ObjectMapper的实现类JsonMapper,功能与ObjectMapper一致。不过新增了一个builder方法。可以直接通过JsonMapper.builder().configure()方法来进行配置,最后获得一个JsonMapper对象。JsonMapper的其他方法基本都集成自ObjectMapper。

注解的使用

上面通过统一配置可对全局格式的序列化和反序列化进行配置,但某些个别的场景下,需要针对具体的字段进行配置,这就需要用注解。比如当Json字符串中的字段与Java对象中的属性不一致时,就需要通过注解来建立它们直接的关系。

@JsonProperty,作用JavaBean字段上,指定一个字段用于JSON映射,默认情况下映射的JSON字段与注解的字段名称相同。可通过value属性指定映射的JSON的字段名称。

@JsonIgnore可用于字段、getter/setter、构造函数参数上,指定字段不参与序列化和反序列化。

@JsonIgnoreProperties作用于类上,序列化时@JsonIgnoreProperties({“prop1”, “prop2”})会忽略pro1和pro2两个属性。反序列化时@JsonIgnoreProperties(ignoreUnknown=true)会忽略类中不存在的字段。

@JsonFormat作用于字段上,通常用来进行格式化操作。

1
2
sql复制代码@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date date;

如果JavaBean中的时间字段使用的是JDK8新增的时间日期(LocalDate/LocalTime/LocalDateTime)类型的话,需要添加jackson-datatype-jsr310依赖。在讲依赖部分时,SpringBoot默认引入的依赖中就有这个。

当然,还有一些其他的注解,比如@JsonPropertyOrder、@JsonRootName、@JsonAnySetter、@JsonAnyGetter、@JsonNaming等,当使用时参考对应的文档和示例看一下就可以,这里就不再一一列出了。

自定义解析器

如果上面的注解和统一配置还无法满足需求,可自定义解析器,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public class MyFastjsonDeserialize extends JsonDeserializer<Point> {

@Override
public Point deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Iterator<JsonNode> iterator = node.get("coordinates").elements();
List<Double> list = new ArrayList<>();
while (iterator.hasNext()) {
list.add(iterator.next().asDouble());
}
return new Point(list.get(0), list.get(1));
}
}

定义完成之后,注册到Mapper中:

1
2
3
4
ini复制代码ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Point.class, new MyFastjsonDeserialize());
objectMapper.registerModule(module);

Jackson处理XML

Jackson也可以通过jackson-dataformat-xml包提供了处理XML的功能。在处理XML时建议使用woodstox-core包,它是一个XML的实现,比JDK自带XML实现更加高效,也更加安全。

1
2
3
4
xml复制代码<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

如果使用Java 9及以上版本,可能会出现java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException异常,这是因为Java 9实现了JDK的模块化,将原本和JDK打包在一起的JAXB实现分隔出来。所以需要手动添加JAXB的实现。

1
2
3
4
xml复制代码<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>

下面是代码示例,基本上和JSON的API非常相似,XmlMapper实际上就是ObjectMapper的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
public void testXml(){
WeChat weChat = new WeChat();
weChat.setId("zhuan2quan");
weChat.setName("程序新视界");
weChat.setInterest(new String[]{"Java", "Spring Boot", "JVM"});

XmlMapper xmlMapper = new XmlMapper();
try {
String xml = xmlMapper.writeValueAsString(weChat);
System.out.println(xml);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}

执行之后,输出结果:

1
2
3
4
5
6
7
8
9
xml复制代码<WeChat>
<id>zhuan2quan</id>
<name>程序新视界</name>
<interest>
<interest>Java</interest>
<interest>Spring Boot</interest>
<interest>JVM</interest>
</interest>
</WeChat>

Spring Boot中的集成

在最开始的时候,我们已经看到Spring Boot默认引入了Jackson的依赖,而且也用我们做什么额外的操作,其实已经在使用Jackson进行Json格式的数据与MVC中参数进行绑定操作了。

如果Spring Boot默认的配置并不适合项目需求,也可以通过内置的配置进行配置,以application.yml配置为例,可通过指定以下属性进行相应选项的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
perl复制代码#指定日期格式,比如yyyy-MM-dd HH:mm:ss,或者具体的格式化类的全限定名
spring.jackson.date-format
#是否开启Jackson的反序列化
spring.jackson.deserialization
#是否开启json的generators.
spring.jackson.generator
#指定Joda date/time的格式,比如yyyy-MM-ddHH:mm:ss). 如果没有配置的话,dateformat会作为backup
spring.jackson.joda-date-time-format
#指定json使用的Locale.
spring.jackson.locale
#是否开启Jackson通用的特性.
spring.jackson.mapper
#是否开启jackson的parser特性.
spring.jackson.parser
#指定PropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)或者指定PropertyNamingStrategy子类的全限定类名.
spring.jackson.property-naming-strategy
#是否开启jackson的序列化.
spring.jackson.serialization
#指定序列化时属性的inclusion方式,具体查看JsonInclude.Include枚举.
spring.jackson.serialization-inclusion
#指定日期格式化时区,比如America/Los_Angeles或者GMT+10.
spring.jackson.time-zone

Spring Boot自动配置非常方便,但某些时候需要我们手动配置Bean来替代自动配置的Bean。可通过如下形式进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Configuration
public class JacksonConfig {
@Bean
@Qualifier("json")
public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper mapper = builder.createXmlMapper(false)
.build();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}

配置完成,可在使用的地方直接注入即可:

1
2
java复制代码@Resource
private ObjectMapper jsonMapper;

对于上面的注入,可能会有朋友问了,是否有线程安全的问题?这个不用担心ObjectMapper是线程安全的。

小结

经过本篇文章的讲解,大家对Jackson应该有一个比较全面的了解了。就个人而言,学习Jackson之后,感觉还是挺有意思的。

原文链接:《放弃FastJson!一篇就够,Jackson的功能原来如此之牛(万字干货)》


程序新视界

\

公众号“ 程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…700701702…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%