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

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


  • 首页

  • 归档

  • 搜索

SpringBoot 之统一异常处理

发表于 2021-11-24

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

统一处理返回结果

主要是利用 SpringMvc的@ControllerAdvice 和 @ExceptionHandler 注解来实现。@ControllerAdvice 声明一个控制器建言, @ExceptionHandler 在方法上注解表示拦截value属性上标明的异常。

当后台在开发过程中,往往需要返回一个json对象给前端。当出现异常时,我们同样希望能把异常按照json格式进行返回,前端就可以根据返回json数据的状态码和信息进行相应的显示。

这时候就需要写一个Http最外层的封装对象,统一返回的对象数据。
Result.java

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
typescript复制代码public class Result<T> {

private Integer code;
private String msg;
private T data;

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}
}

根据返回数据对象可封装相应的结果模板工具类
ResultUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码public class ResultUtil {

public static Result getOK(Object object){
Result result = new Result();
result.setCode(0);
result.setMsg("成功");
result.setData(object);
return result;
}

public static Result getOK(){
return getOK(null);
}

public static Result getError(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
}

当后台对前端传过来的数据进行判断,返回相应的结果时,可以进行统一异常处理。
将结果抛出,如果直接进行抛出的话,结果并不是之前我们返回的json格式,这样的处理也不友好。我们可以通过对异常的捕获,调用结果模板类(ResultUtil.java)即可返回我们封装好的json数据。

异常捕获类ExceptionHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码import com.demo.result.Result;
import com.demo.result.ResultUtil;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class ExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handle(Exception e){
return ResultUtil.getError(100,e.getMessage());
}

}

如果只是利用默认Exception进行抛出结果,这样返回的状态码(code)每次都是同一个值,前端处理的时候就不好处理。这样就需要自定义一个Exception。注意这里不能继承Exception,因为 springBoot只支持继承RuntimeException的。

自定义Exception类UserException.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码public class UserException extends RuntimeException {

private Integer code;

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public UserException(Integer code,String message) {
super(message);
this.code = code;
}
}

此时异常捕获类ExceptionHandler.java也需要更改

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
kotlin复制代码import com.demo.exception.UserException;
import com.demo.result.Result;
import com.demo.result.ResultUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class ExceptionHandler {

//增加异常日志打印
private final static Logger logger = LoggerFactory
.getLogger(ExceptionHandle.class);

@ExceptionHandler(Exception.class)
@ResponseBody
public Result handle(Exception e){
if(e instanceof UserException){
UserException userException = (UserException)e;
return ResultUtil.getError(userException.getCode()
,userException.getMessage());
}else{
logger.error("【系统异常】={}",e);
return ResultUtil.getError(-1,"未知错误!");
}
}

}

通过对结果的统一处理,可以很友好的将数据返回给前端,但此刻发现一个问题,就是每次返回数据时,状态码和信息都要在调用方法中重新定义,这样做的话状态多一点,查看和修改就有了困难。为此可定义一个枚举类,统一管理code和msg。

ResultEnum.java

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
arduino复制代码public enum ResultEnum {
/**
* 成功. ErrorCode : 0
*/
SUCCESS("0","成功"),
/**
* 未知异常. ErrorCode : 01
*/
UnknownException("01","未知异常"),
/**
* 系统异常. ErrorCode : 02
*/
SystemException("02","系统异常"),
/**
* 业务错误. ErrorCode : 03
*/
MyException("03","业务错误"),
/**
* 提示级错误. ErrorCode : 04
*/
InfoException("04", "提示级错误"),
/**
* 数据库操作异常. ErrorCode : 020001
*/
DBException("020001","数据库操作异常"),
/**
* 参数验证错误. ErrorCode : 040001
*/
ParamException("040001","参数验证错误");

private String code;

private String msg;

ResultEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}

public String getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

本文转载自: 掘金

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

SpringBoot集成Swagger(六)玩转groupN

发表于 2021-11-24

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」


相关文章

Java随笔记:Java随笔记


前言

  • 关于过滤的东西我们全部讲完了,今天主要来讲下分组。
  • 当项目过去庞大时,一个组可能不能满足我们的需求。
  • 我们需要对项目的不同功能进行分组,方便查看和区分。

groupName()分组

  • 首先我们看下,默认的有哪些分组?启动项目。
  • image-20211124214840836.png
  • 默认只有一个分组default,默认分组。
  • 源码如下:
+ 
1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码​
 /**
  * If more than one instance of Docket exists, each one must have a unique groupName as
  * supplied by this method. Defaults to "default".
  *
  * @param groupName - the unique identifier of this swagger group/configuration
  * @return this Docket
  */
 public Docket groupName(String groupName) {
   this.groupName = defaultIfAbsent(groupName, this.groupName);
   return this;
}
  • 那么this.groupName的值呢?
+ ![image-20211124215054088.png](https://gitee.com/songjianzaina/juejin_p6/raw/master/img/e15b1494e9436010b0430713c010510bbd2cb08bcd32cb75861a7fdb7a5a87b7)
  • 由此可以得出,当我们不设定groupName()时,会有且只有一个默认分组。
  • 自定义分组名称
+ 
1
2
3
4
5
6
7
8
9
10
scss复制代码    @Bean
   public Docket docket(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .groupName("group1")
              .enable(swaggerModel.isEnable())
              .select()
              .paths(PathSelectors.any())
              .build();
  }
  • 再次启动项目看看
+ ![image-20211124215227175.png](https://gitee.com/songjianzaina/juejin_p6/raw/master/img/a2d8e91aa9508181c81f3c412f651b9fbf3d899b36c9465734406a124fb5b625)
  • ok!没问题了,成功设定了分组!
  • 那么,如果我们有多个分组该怎么办呢?
  • 前面的文章就讲到了,我们是使用Docket来控制Swagger的配置的。
  • 只需要设定多个Docket即可。
  • 代码如下:
+ 
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
scss复制代码    @Bean
   public Docket docket1(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .groupName("group1")
              .enable(swaggerModel.isEnable())
              .select()
              .paths(PathSelectors.any())
              .build();
  }
​
   @Bean
   public Docket docket2(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .groupName("group2")
              .enable(swaggerModel.isEnable())
              .select()
              .paths(PathSelectors.any())
              .build();
  }
​
   @Bean
   public Docket docket3(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .groupName("group3")
              .enable(swaggerModel.isEnable())
              .select()
              .paths(PathSelectors.any())
              .build();
  }
+ 再次重启项目 - ![image-20211124215506103.png](https://gitee.com/songjianzaina/juejin_p6/raw/master/img/049b50901c83a8368965fd5564b5ebd2d8dc665d033b0014e937eae5d581e6a1) + nice!多个分组设置成功!

总结

  • 如果是在实际项目开发中呢?
  • 因为有多个Docket,每个Docket都可以设置一个分组,当然也可以设定每个Docket的单独过滤规则
  • 这样就完美设置成了一个大的功能模块对应一个分组。
  • 方便前端根据功能来查找对应的接口信息。
  • 为什么要这样?
  • 因为项目大了之后可能有几百上千个接口。如果全在一个组内,找起来特别麻烦!
  • 而groupName()可以很方便的帮我们规避掉这个问题!
  • 以上都是个人所言,如有不对,欢迎指出。
  • 如果对您有帮助,希望给我点个赞点个关注呗!咱们明天继续Swagger的讲解!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

ElasticSearch进阶四:分片的操作流程 一、写流程

发表于 2021-11-24

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」

一、写流程

新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
image.png

新建,索引和删除文档所需要的步骤顺序:

  1. 端向 Node 1 发送新建、索引或者删除请求
  2. 文档的_id确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。

这些选项很少使用,因为Elasticsearch已经很快,如下所示:

参数 含义
consistency consistency,即一致性。在默认设置下,即使仅仅是在试图执行一个_写_操作之前,主分片都会要求 必须要有 规定数量(quorum)(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行_写_操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(network partition)的时候进行_写_操作,进而导致数据不一致。_规定数量_即:int( (primary + number_of_replicas) / 2 ) + 1consistency 参数的值可以设为 one (只要主分片状态 ok 就允许执行_写_操作),all(必须要主分片和所有副本分片的状态没问题才允许执行_写_操作), 或 quorum 。默认值为 quorum , 即大多数的分片副本状态没问题就允许执行_写_操作。注意,规定数量 的计算公式中 number_of_replicas 指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有三个副本分片,那规定数量的计算结果即:int( (primary + 3 replicas) / 2 ) + 1 = 3如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此您将无法索引和删除任何文档。
timeout 如果没有足够的副本分片会发生什么? Elasticsearch会等待,希望更多的分片出现。默认情况下,它最多等待1分钟。 如果你需要,你可以使用 timeout 参数 使它更早终止: 100 100毫秒,30s 是30秒。

Tips:
新索引默认有 1 个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of_replicas 大于1的时候,规定数量才会执行。

二、读流程

我们可以从主分片或者从其它任意副本分片检索文档

image.png

从主分片或者副本分片检索文档的步骤顺序:

  1. 客户端向 Node 1 发送获取请求
  2. 节点使用文档的_id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2
  3. Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端
    在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

三、更新流程

部分更新一个文档结合了先前说明的读取和写入流程:

image.png

部分更新一个文档的步骤如下:

  1. 客户端向 Node 1 发送更新请求。
  2. 它将请求转发到主分片所在的 Node3 。
  3. Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。
  4. 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

Tips:
当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果Elasticsearch仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

四、多文档操作流程

mget 和 bulk API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。

协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端

image.png

用单个 mget 请求取回多个文档所需的步骤顺序:

  1. 客户端向 Node 1 发送 mget 请求。
  2. Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
    可以对 docs 数组中每个文档设置 routing 参数。

bulk API, 允许在单个批量请求中执行多个创建、索引、删除和更新请求。

image.png

bulk API 按如下步骤顺序执行:

  1. 客户端向 Node 1 发送 bulk 请求。
  2. Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
  3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。

本文转载自: 掘金

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

Nodejs 应用全链路追踪实践 Nodejs 应用全链

发表于 2021-11-24

Node.js 应用全链路追踪

全链路追踪技术的两个核心要素分别是全链路信息获取和全链路信息存储展示。

本文一共分为三个篇章进行介绍;

  • 第一章介绍Nodejs应用全链路信息获取。
  • 第二章介绍Node.js 应用全链路追踪实战。

Node.js 应用全链路追踪系统

一、简介

目前主流的 Node.js 架构设计主要有以下两种方案:

  • 通用架构:只做 ssr 和 bff,不做服务器和微服务;
  • 全场景架构:包含 ssr、bff、服务器、微服务。

上述两种方案对应的架构说明图如下图所示:

图片

在上述两种通用架构中,nodejs 都会面临一个问题,那就是:

在请求链路越来越长,调用服务越来越多,其中还包含各种微服务调用的情况下,出现了以下诉求:

  • 如何在请求发生异常时快速定义问题所在;
  • 如何在请求响应慢的时候快速找出慢的原因;
  • 如何通过日志文件快速定位问题的根本原因。

我们要解决上述诉求,就需要有一种技术,将每个请求的关键信息聚合起来,并且将所有请求链路串联起来。让我们可以知道一个请求中包含了几次服务、微服务请求的调用,某次服务、微服务调用在哪个请求的上下文。

这种技术,就是Node.js应用全链路追踪。它是 Node.js 在涉及到复杂服务端业务场景中,必不可少的技术保障。

综上,我们需要Node.js应用全链路追踪,说完为什么需要后,下面将介绍如何做Node.js应用的全链路信息获取。

二、全链路信息获取

全链路信息获取,是全链路追踪技术中最重要的一环。只有打通了全链路信息获取,才会有后续的存储展示流程。

对于多线程语言如 Java 、 Python 来说,做全链路信息获取有线程上下文如 ThreadLocal 这种利器相助。而对于Node.js来说,由于单线程和基于IO回调的方式来完成异步操作,所以在全链路信息获取上存在天然获取难度大的问题。那么如何解决这个问题呢?

三、业界方案

由于 Node.js 单线程,非阻塞 IO 的设计思想。在全链路信息获取上,到目前为止,主要有以下 4 种方案:

  • domain: node api;
  • zone.js: Angular 社区产物;
  • 显式传递:手动传递、中间件挂载;
  • Async Hooks:node api;

而上述 4 个方案中, domain 由于存在严重的内存泄漏,已经被废弃了;zone.js 实现方式非常暴力、API比较晦涩、最关键的缺点是 monkey patch 只能 mock api ,不能 mock language;显式传递又过于繁琐和具有侵入性;综合比较下来,效果最好的方案就是第四种方案,这种方案有如下优点:

  • node 8.x 新加的一个核心模块,Node 官方维护者也在使用,不存在内存泄漏;
  • 非常适合实现隐式的链路跟踪,入侵小,目前隐式跟踪的最优解;
  • 提供了 API 来追踪 node 中异步资源的生命周期;
  • 借助 async_hook 实现上下文的关联关系;

优点说完了,下面我们就来介绍如何通过 Async Hooks 来获取全链路信息。

四、async hooks

官方文档描述 async_hooks: 它被用来追踪异步资源,也就是监听异步资源的生命周期。

The async_hooks module provides an API to track asynchronous resources.

既然它被用来追踪异步资源,则在每个异步资源中,都有两个 ID:

  • asyncId: 异步资源当前生命周期的 ID
  • trigerAsyncId: 可理解为父级异步资源的 ID,即 parentAsyncId

通过以下 API 调取

1
2
3
4
5
6
ini复制代码const async_hooks = require('async_hooks');
​
const asyncId = async_hooks.executionAsyncId();
​
const trigerAsyncId = async_hooks.triggerAsyncId();
​

更多详情参考官方文档: async_hooks API

既然谈到了 async_hooks 用以监听异步资源,那会有那些异步资源呢?我们日常项目中经常用到的也无非以下集中:

  • Promise
  • setTimeout
  • fs/net/process 等基于底层的API

然而,在官网中 async_hooks 列出的竟有如此之多。除了上述提到的几个,连 console.log 也属于异步资源: TickObject。

1
2
3
4
5
复制代码FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject

async_hooks.createHook

我们可以通过 asyncId 来监听某一异步资源。

通过 async_hooks.createHook 创建一个钩子,事例代码:

1
2
3
4
5
6
7
8
9
less复制代码const asyncHook = async_hooks.createHook({
 // asyncId: 异步资源Id
 // type: 异步资源类型
 // triggerAsyncId: 父级异步资源 Id
 init (asyncId, type, triggerAsyncId, resource) {},
 before (asyncId) {},
 after (asyncId) {},
 destroy(asyncId) {}
})

我们只需要关注最重要的四个 API:

  • init: 监听异步资源的创建,在该函数中我们可以获取异步资源的调用链,也可以获取异步资源的类型,这两点很重要。
  • destory: 监听异步资源的销毁。要注意 setTimeout 可以销毁,而 Promise 无法销毁,如果通过 async_hooks 实现 CLS(Continuation-local Storage) 可能会在这里造成内存泄漏!
  • before
  • after
1
2
3
4
5
6
7
8
9
10
scss复制代码setTimeout(() => {
 // after 生命周期在回调函数最前边
 console.log('Async Before')
 op()
 op()
 op()
 op()
 // after 生命周期在回调函数最后边
 console.log('Async After')
})

async_hooks 调试及测试

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
javascript复制代码const fs = require('fs')
const async_hooks = require('async_hooks')
async_hooks.createHook({
 init (asyncId, type, triggerAsyncId, resource) {
   fs.writeSync(1, `${type}(${asyncId}): trigger: ${triggerAsyncId}\n`)
},
 destroy (asyncId) {
   fs.writeSync(1, `destroy: ${asyncId}\n`);
}
}).enable()
async function A () {
 fs.writeSync(1, `A -> ${async_hooks.executionAsyncId()}\n`)
 setTimeout(() => {
   fs.writeSync(1, `A in setTimeout -> ${async_hooks.executionAsyncId()}\n`)
   B()
})
}
async function B () {
 fs.writeSync(1, `B -> ${async_hooks.executionAsyncId()}\n`)
 process.nextTick(() => {
   fs.writeSync(1, `B in process.nextTick -> ${async_hooks.executionAsyncId()}\n`)
   C()
   C()
})
}
function C () {
 fs.writeSync(1, `C -> ${async_hooks.executionAsyncId()}\n`)
 Promise.resolve().then(() => {
   fs.writeSync(1, `C in promise.then -> ${async_hooks.executionAsyncId()}\n`)
})
}
fs.writeSync(1, `top level -> ${async_hooks.executionAsyncId()}\n`)
A()

async_hooks.createHook 可以注册 4 个方法来跟踪所有异步资源的初始化(init)、回调之前(before)、回调之后(after)、销毁后(destroy)事件,并通过调用 .enable() 启用,调用 .disable() 关闭。

这里我们只关心异步资源的初始化和销毁的事件,并使用 fs.writeSync(1, msg) 打印到标准输出,writeSync 的第 1 个参数接收文件描述符,1 表示标准输出。为什么不使用 console.log 呢?因为 console.log 是一个异步操作,如果在 init、before、after 和 destroy 事件处理函数中出现,就会导致无限循环,同理也不能使用任何其他的异步操作。

运行该程序,打印如下:

image-20211026190904803

本文转载自: 掘金

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

学习 DDD 之消化知识!

发表于 2021-11-24

接触到DDD到现在已经有8个月份了,目前所维护的项目也是基于DDD的思想开发的,从一开始的无从下手,到现在游刃有余,学到不少东西,但是都是一些关键字和零散的知识,同时我也感受到了是因为我对项目越来越熟悉,熟能生巧导致我现在在做需求的时候根本不用过多的去思考,就能很好的完成业务需求,我慢慢的意识到,学习DDD是非常有必要的。

在传统的开发模式中,产品经理在跟业务专家沟通业务需求后,对其进行抽象并将结果通过口头或者项目管理工具传达到开发人员,开发人员根据产品经理传递的业务需求机械式地进行功能开发,这样的模式使开发人员没有真正的理解业务原理,开发出来的功能就很难达到业务方的要求,即使达到要求也难以应对未来的业务变化。

如果大家都运用DDD的思想进行开发,就能很好的传递业务知识,因为DDD倡导开发人员、产品经理、领域专业一起讨论、消化业务知识,彻底理解业务原理。

以下是我在学习DDD时做的一些笔记,并整理成思维导图的形式,这样就能很好的形成结构化的思维,希望能让大家对DDD有一个更深的理解。

以下内容部分摘自 《领域驱动设计》和根据自己的理解整理而成。

1.1 有效建模的要素

模型和现实绑定

开发人员与产品经理在讨论需求的时候,都会画一些草图和对草图做一些文字说明,这其实就是最初的模型。最初的原型虽然简陋,但它在模型与与实现之间建立了早期链接,而且在所有的后续迭代中我们一直在维护该链接。

建立一种基于模型的语言

  1. 起初领域专家不得不向开发人员解释业务知识
  2. 开发人员也必需向领域专家解释类图的含义
  3. 随着项目的进展,双方都能够使用模型中的术语,并将它们组织成符合模型结构的语句 ,而且可以无需翻译就能互相理解要表达的意思

领域专家专注业务领域,开发人员专注开发,两个不同领域的人在没有形成统一语言的前提下是很难沟通的,比如电商领域专家抛出订单履约的概念的时候,不得不在解释什么是订单履约时,还得向开发人员翻译里面的各种名词,如果领域专家和开发人员知识对等,就能互相理解各自要表达的意思。

开发一个蕴含丰富知识的模型

  1. 对象具有行和为强制性的规定
  2. 模型并不仅仅是一种数据模型
  3. 模型应包含各种类型的知识

提炼模型

在模型日趋完整的过程中,要提炼模型,要将新的概念添加到模型中,同时将不再使用的或者不重要的概念从模型中移除。

头脑风暴和实验

  1. 语言和草图,再加上头脑风暴,将我们的讨论变成“模型实验室”在这些讨论中可以演示、尝试和判断上百种变化
  2. 当团队走查场景时,口头表达本身就可以作为所提议模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简洁还是表达的笨拙

1.2 消化知识

高效的领域建模人员是知识的消化者

建模人员需要从大量的信息中寻找有有的部分,然后不断的尝试各种信息组织方式,努力寻找对大量信息有意义的知识。

知识消化并非一项孤立的活动

  1. 一般由开发人员领导下,由开发人员与领域专家组成团队来共同协作。
  2. 共同收集信息,并通过消化而将它们组织为有用的形式
  3. 信息的原始资源来自领域专家头脑中的知识、现有用户、以及技术团队在相关遗留系统或者同领域其他项目中积累的经验

传统瀑布模式的不足

业务专家与分析员(产品经理)进行讨论,分析员消化理解这些业务知识后,对其进行抽象并将结果传递给程序员,再由程序员编写软件代码。

  1. 这种方法完全没有反馈(程序员没有提供自己的想法)
  2. 分析员全权负责创建模型,但他们创建的模型只是基于业务专家的建议
  3. 分析员没有向程序员学习,得不到早期版本的经验
  4. 知识只是朝一个方向流动,不会累积

领域专家与开发人同一起消化理解模型的好处

在团队所有成员一起消化理解模型的过程中,他们之间的交互也会发生变化。

  1. 领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发
  2. 领域专家被迫提炼自己已知道的重要知识的过程往往也是完善其自身理解的过程,而且他们会渐渐理解软件项目所必需的概念严谨性。

小结

开发人员、分析员、领域专家,都应该将自己的知识输入到模型中,这样模型的组织更严密,抽象也更为整洁。

模型不断改进的同时 ,也成为组织项目信息流的工具。模型聚焦于需求分析,它与编码和设计紧密交互。

1.3 持续学习

当开始编写软件时,其实我们所知甚少

项目知识零散地分散在很多人和文档中,其中夹杂着其他一些无用的信息,因此我们甚至不知道哪些知识是真正需要的知识。

看起来没有什么技术难度的领域很可能是一种错觉 – 我们并没有意识到不知道的东西究竟有多少。这种无知往往会导致我们做出错误的判断。

所有的项目都会丢失知识

  1. 已经学到了一些知识的人可能去干别的事了
  2. 团队由于重组而被拆散,这导致知识又被重新分散开
  3. 被外包出去的关键子系统可能只交回了代码,而不会将知识传递回来

当使用典型的设计方法时,代码和文档不会以一种有用的形式表示出这些来之不易的知识。因此一但由于某些原因团队成员没有口头传递知识,那么知识就会丢失。

高效率的团队需要有意识地积累知识,并持续学习

对于开发人员来说, 这意味着既要完善技术知识,也要培养一般的领域建模技巧。但这也包括认真学习他们正在正在从事的特定领域知识。

那些善于自学的团队成员会成为团队的中坚力量,涉及最关键领域的开发任务要靠他们来攻克。这个核心团队头脑中积累的知识使他们成为更高效的知识消化者。

1.4 知识丰富的设计

  1. 业务活动和规则如同所涉及的实体一样,但是领域的核心,任何领域都有各种类别的概念。
  2. 知识消化所产生的模式,能够反映出对知识的深层理解。
  3. 在模型发生改变的同时,开发人员对实现进行重构,以便反映出模型的变化,这样,新知道就被合并到应用程序中了

1.5 深层模型

有用的模式很少停留在表面上, 随着对领域和应用程序需求的理解逐步加深,我们往往会丢最初看起来很重要的表面元素,或者切换它们的角度。这时,一些开始不可能发现的巧妙抽象就会渐渐的浮出水面, 而它们恰恰切中问题的要害。

推荐

  • 分享一套家庭理财系统(附源码)
  • 推荐一套开源通用后台管理系统(附源码)
  • 推荐一个酷炫的监控系统
  • 从朋友那里搞了 20 个实战项目,速领!
  • 推荐一个完善的停车管理系统,物联网项目springboot,附源码
  • 推荐一个互联网企业级别的开源支付系统
  • 一款神仙接私活儿软件,吊到不行!
  • 推荐 10 款超实用的企业级开源应用!
  • 开放平台 SDK 设计实践!
  • “淘宝” 开放平台接口设计思路
  • Spring中经典的9种设计模式

本文转载自: 掘金

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

『十倍程序员』Docker部署kafka+zookeeper

发表于 2021-11-24

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」。

点赞再看,养成习惯👏👏

前言

Hello 大家好,我是l拉不拉米,今天的『十倍程序员』系列给大家分享Docker部署kafka+zookeeper的实战。

环境准备

文章使用 Window 系统 + Docker Desktop 演示,假设您有一定的docker基础。

Docker Desktop

Docker Desktop为我们学习Docker提供了一套完整的一个桌面环境,可以为软件开发提供很多便利。

Docker Desktop包含了Docker Engine, Docker CLI client, Docker Compose, Docker Machine和Kitematic。

先去 Docker官网 下载 Docker Desktop 并安装。

docker-compose

Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

compose命令

命令 解释
docker-compose up 启动所有容器
docker-compose up -d 后台启动并运行所有容器
docker-compose up –no-recreate -d 不重新创建已经停止的容器
docker-compose up -d test2 只启动test2这个容器
docker-compose stop 停止容器
docker-compose start 启动容器
docker-compose down 停止并销毁容器

由于安装Docker Desktop时会默认安装 docker-compose,所以我们并不需要特意去安装compose。

编写docker-compose.yml

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
yml复制代码version: '3'
services:
zookeeper:
image: wurstmeister/zookeeper
volumes:
- ./data:/data
ports:
- "2181:2181"

kafka:
image: wurstmeister/kafka
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_MESSAGE_MAX_BYTES: 2000000
KAFKA_CREATE_TOPICS: "Topic1:1:3,Topic2:1:1:compact"
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
volumes:
- ./kafka-logs:/kafka
- /var/run/docker.sock:/var/run/docker.sock

kafka-manager:
image: sheepkiller/kafka-manager
ports:
- 9020:9000
environment:
ZK_HOSTS: zookeeper:2181

将docker-compose.yml放到任意文件目录下。

window终端执行Docker Compose命令

启动window终端,进入到docker-compose.yml 所在的文件目录

服务打包

1
2
3
arduino复制代码[root@rameo kafka] # docker-compose build
zookeeper uses an image, skipping
kafka uses an image, skipping

启动服务

1
2
3
bash复制代码[root@rameo kafka]# docker-compose up -d
Starting kafka_kafka_1 ... done
Starting kafka_zookeeper_1 ... done

此时已经启动成功,我们可以通过Docker Desktop桌面工具看拉取的镜像

image.png

和启动的容器

image.png

总结

通过这个例子,我们可以非常高效的实现kafka集群+zookeeper集群的搭建,主要得以与Docker Desktop工具集成了很多docker能力和docker-compose编排处理的能力,我们不需要每一个镜像拉取、运行、配置一次,而是全都由统一的docker-compose.yml集中管理,一次运行。

本文转载自: 掘金

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

若依系统分页工具学习-PageHelper篇十二

发表于 2021-11-24

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

自PageHelper篇十一开始,我们开始剖析PageHelper的文件结构与类结构。

在昨天的文章中,我们了解了PageHelper中的包com.github.pagehelper中的几个类。

今天我们来看pagehelper中几个子包中的类。

我们先来看看pagehelper的几个子包:

1
2
3
4
5
java复制代码com.github.pagehelper.cache, 
com.github.pagehelper.dialect,
com.github.pagehelper.page,
com.github.pagehelper.parser,
com.github.pagehelper.util`

cache包

顾名思义,这个包应该和缓存有关。其中包括:接口Cache<K, V>, 抽象工厂类CacheFactory,实现Cache<K, V>接口的SimpleCache<K, V>, GuavaCache<K, V>。

接口Cache<K, V>中定义了两个基本方法:get, set。
.
抽象工厂类CacheFactory中有一个静态方法createCache,返回值类型为接口Cache<K, V>。在createCache中用到了一个不太常用的Java中的公共方法Class.forName(full-className)。

SimpleCache<K, V>中定义了一个无法修改的变量CACHE,一个构造器SimpleCache(Properties properties, String prefix),以及实现接口Cache<K, V>中的get, set方法。

GuavaCache<K, V>中同样定义了一个无法修改的变量CACHE,一个构造器GuavaCache(Properties properties, String prefix),以及实现接口Cache<K, V>中的get, set方法。

两个缓存实现类中的不同在于SimpleCache中的CACHE类型为:org.apache.ibatis.cache.Cache<K, V>,而GuavaCache中其类型为:com.google.common.cache.Cache<K, V>。

我们先来看一下工厂类CacheFactory中的createCache方法:

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
java复制代码/**
* 创建 SQL 缓存
*
* @param sqlCacheClass
* @return
*/
public static <K, V> Cache<K, V> createCache(String sqlCacheClass, String prefix, Properties properties) {
if (StringUtil.isEmpty(sqlCacheClass)) {
try {
Class.forName("com.google.common.cache.Cache");
return new GuavaCache<K, V>(properties, prefix);
} catch (Throwable t) {
return new SimpleCache<K, V>(properties, prefix);
}
} else {
try {
Class<? extends Cache> clazz = (Class<? extends Cache>) Class.forName(sqlCacheClass);
try {
Constructor<? extends Cache> constructor = clazz.getConstructor(Properties.class, String.class);
return constructor.newInstance(properties, prefix);
} catch (Exception e) {
return clazz.newInstance();
}
} catch (Throwable t) {
throw new PageException("Created Sql Cache [" + sqlCacheClass + "] Error", t);
}
}
}

通过是否传入sqlCacheClass决定两个逻辑。

当sqlCacheClass不指定时,先尝试创建GuavaCache,创建失败时再创建SimpleCache。创建GuavaCache之前为什要执行下面这行代码呢?

1
java复制代码Class.forName("com.google.common.cache.Cache");

Java官方文档对方法static Class<?> forName(String className)的解释是:

返回与带有给定字符串名的类或接口相关联的 Class 对象。

如果我们对Java中数据库链接熟悉的话会更清楚这一点。

一般我们连接Mysql时,程序会有这么几行:

1
java复制代码Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(url, username, password);

而com.mysql.jdbc.Driver中有这样一段静态代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

而类的初始化过程中会执行一遍类的静态语句,包括静态变量的声明、静态代码块等。

那么问题来了,com.google.common.cache.Cache是什么样的呢?

本文转载自: 掘金

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

通过Docker快速搭建NuGet私仓

发表于 2021-11-24

通用封面.png

前言

无论是Java还是.Net都有自己的包管理工具,在.Net世界里大家最常用的就是NuGet了,有人可能要问微软官方已经提供NuGet包管理平台,也非常好用,为啥一定要自己搭建一个私有的包管理仓库呢?当然了,凡是存在必然有其合理性,比如:很多大型公司严格限制公网环境;或者公司处于自我保护目的只允许在公司内部使用相关核心的程序;还有可能仅仅是因为程序员想方便自己开发。这些情况都迫使我们需要有个私有的包管理工具,来方便管理我们的工程项目的包的引用。

市面上有很多种工具都可以实现此功能,本文以BaGet为例通过Docker的方式快速搭建一个本地私有包管理器。操作如下:

环境搭建

1.初始化配置文件

首先找一个存放配置文件和包文件夹的路径。 举例存放路径为:D:\wsl\docker-volume\baget。在此路径下新增配置文件和文件夹:

  • 配置文件命名为:baget.env
  • 文件夹命名:baget-data

0.init.png

在baget.env文件中添加如下配置信息,其中ApiKey为秘钥(推送包时会用到),可以自己调整。

1
2
3
4
5
6
7
8
9
ini复制代码# The following config is the API Key used to publish packages.
# You should change this to a secret value to secure your server.
ApiKey=NUGET-SERVER-API-KEY

Storage__Type=FileSystem
Storage__Path=/var/baget/packages
Database__Type=Sqlite
Database__ConnectionString=Data Source=/var/baget/baget.db
Search__Type=Database

2.Docker启动镜像

在此目录下打开PowerShell,运行命令如下:

1
css复制代码docker run -d --name nuget-server -p 5555:80 --env-file baget.env -v "$(pwd)/baget-data:/var/baget"

该命令会做如下事情:

  • -d 后台运行
  • –name 容器名字为nuget-server
  • -p 映射端口到5555
  • -env-file 读取配置文件信息;
  • -v 映射卷文件到baget-data文件夹

3.验证是否成功

如访问地址http://localhost:5555后出现下图则说明搭建成功。

1.login.png
红色字体为仓库上传地址。
2.upload.png

生成自己的包文件

测试阶段随便找一个类库项目,右键点击打包。

3.package.png

在bin目录下会生成包文件。

4.package-detail.png

发布包文件到私仓

在包文件所在目录下执行如下命令,把包文件推送到私仓中:

1
bash复制代码dotnet nuget push -s http://localhost:5555/v3/index.json -k NUGET-SERVER-API-KEY AbpFreesqlnew.Domain.1.0.0.nupkg

如果配置文件中调整了ApiKey的秘钥值,则在命令中 需要同步替换NUGET-SERVER-API-KEY为自己的秘钥。 如图所示我设定秘钥为: 9876

5.upload-powershell.png
回到网页端可以查询到上传成功的包文件。

6.upload-success.png

添加私仓地址

打开NuGet包管理器(工具-NuGet包管理器-管理解决方案的NuGet的程序包),在设置页面中添加私仓地址:http://localhost:5555/v3/index.json。

nuget-setting.png

add-new-nuget.png
此时已经可以查询到我们上传的包文件,可以在项目中直接使用。

display-localnuget.png

延伸

通过以上步骤我们很快就搭建好了私有NuGet仓库,下次再使用时在Docker中直接启动容器即可。前文提到有很多工具都可以实现私仓的搭建,选则时可以按自己所需选择,如果想同时管理Java、.Net、Npm 建议选择Nexus,可以同时满足很多场景,有兴趣的小伙伴可以试着搭一次。

本文转载自: 掘金

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

分库分表技术之ShardingJDBC(1)

发表于 2021-11-24

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

分库分表方式回顾

分库分表的目的就是将我们的单库的数据控制在合理范围内,从而提高数据库的性能。

  • 垂直拆分(按照结构分)
    • 垂直分表:将一张宽表(字段很多的表),按照字段的访问频次进行拆分,就是按照表单结构进行拆
    • 垂直分库:根据不同的业务,将表进行分类,拆分到不同的数据库。这些库可以部署在不同的服务器,分摊访问压力。
  • 水平拆分(按照数据行分)
    • 水平分库:将一张表的数据(按照数据行)分到多个不同的数据库。每个库的表结构相同。每个库都只有这张表的部分数据,当单表的数据量过大,如果继续使用水平分库,那么数据库的实例就会不断增加,不利于系统的运维。这时候就要采用水平分表。
    • 水平分表:将一张表的数据(按照数据行),分配到同一个数据库的多张表中,每个表都只有一部分数据。

什么时候用分库分表

  • 在系统设计阶段,就要完成垂直分库和垂直分表。在数据量不断上升,数据库性能无法满足需求的时候,首先要考虑的是缓存、读写分离、索引技术等方案。如果数据量不断增加,并且持续增长再考虑水平分库水平分表。

分库分表带来的问题

关系型数据库在单机单库的情况下,比较容易出现性能瓶颈问题,分库分表可以有效的解决这方面的问题,但是同时也会产生一些比较棘手的问题。

事务一致性问题

当我们需要更新的内容同时分布在不同的库时,不可避免的会产生跨库的事务问题。原来在一个数据库操作,本地事务就可以进行控制,分库之后 一个请求可能要访问多个数据库,如何保证事务的一致性,目前还没有简单的解决方案。

跨节点关联的问题

在分库之后,原来在一个库中的一些表,被分散到多个库,并且这些数据库可能还不在一台服务器,无法关联查询。

解决这种关联查询,需要我们在代码层面进行控制,将关联查询拆开执行,然后再将获取到的结果进行拼装。

分页排序查询的问题

分库并行查询时,如果用到了分页每个库返回的结果集本身是无序的,只有将多个库中的数据先查出来,然后再根据排序字段在内存中进行排序,如果查询结果过大也是十分消耗资源的。

主键避重问题

在分库分表的环境中,表中的数据存储在不同的数据库,主键自增无法保证ID不重复,需要单独设计全局主键。

公共表的问题

不同的数据库,都需要从公共表中获取数据。可以在每一个库都创建这个公共表,所有对公共表的更新操作,都同时发送到所有分库执行。ShardingJDBC可以帮助我们解决这个问题。

ShardingJDBC简介

什么是ShardingJDBC

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成,我们只关注Sharding-JDBC即可。

官方地址:shardingsphere.apache.org/document/cu…

Sharding-JDBC 定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架的使用。

  • 适用于任何基于Java的ORM框架,如:JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。
  • 基于任何第三方的数据库连接池,如:DBCP、C3P0、Druid等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL、Oracle、SQLServer和PostgreSQL。

image.png

上图展示了Sharding-Jdbc的工作方式,使用Sharding-Jdbc前需要人工对数据库进行分库分表,在应用程序中加入Sharding-Jdbc的Jar包,应用程序通过Sharding-Jdbc操作分库分表后的数据库和数据表,由于Sharding-Jdbc是对Jdbc驱动的增强,使用Sharding-Jdbc就像使用Jdbc驱动一样,在应用程序中是无需指定具体要操作的分库和分表的。

Sharding-JDBC主要功能

  • 数据分片
  • 读写分离

通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。

Sharding-JDBC与MyCat的区别

  1. mycat是一个中间件的第三方应用,sharding-jdbc是一个jar包
  2. 使用mycat时不需要修改代码,而使用sharding-jdbc时需要修改代码
  3. Mycat是基于 Proxy,它复写了MySQL协议,将Mycat Server伪装成一个MySQL数据库,而Sharding-JDBC是基于JDBC的扩展,是以jar包的形式提供轻量级服务的。
  • Mycat(proxy中间件层)

image.png

  • Sharding-jdbc(应用层)

image.png

本文转载自: 掘金

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

手动实现一下Mysql读写分离

发表于 2021-11-24

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

背景

当数据量过大时候,对单表进行更新、查询的操作有时候会导致锁表,让读写速度跟不上,一个页面就要2-3秒,这就需要使用读写分离。
很多应用的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,读操作从数据库来进行。

代码

配置文件

增加双数据源的数据库配置

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
yml复制代码spring:
datasource:
datasource1:
url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
username: admin
password: 1024571
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initial-size: 1
min-idle: 1
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
datasource2:
url: jdbc:mysql://localhost:3307/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
username: admin
password: 1024571
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initial-size: 1
min-idle: 1
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
自定义注解

自定义数据源key的注解,value为数据源key

1
2
3
4
5
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSource {
String value() default "data1";
}
数据源key设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Slf4j
public class DataSourceContextHolder {

private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

// 设置数据源名称
public static void setDataSource(String dataSource){
contextHolder.set(dataSource);
}

public static String getDataSource(){
return contextHolder.get();
}

// 清除数据源
public static void clearDataSource(){
contextHolder.remove();
}

}
动态数据源类
1
2
3
4
5
6
java复制代码public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
数据源配置类

定义双数据源的key和bean对应关系

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
java复制代码@Configuration
public class DataSourceConfig {
/**
* 数据源1
*/
@Bean(name = "data1")
@ConfigurationProperties(prefix = "spring.datasource.data1")
public DataSource Data1(){
return DataSourceBuilder.create().build();
}

/**
* 数据源2
*/
@Bean(name = "data2")
@ConfigurationProperties(prefix = "spring.datasource.data2")
public DataSource Data2(){
return DataSourceBuilder.create().build();
}

/**
* 数据源切换: 通过AOP在不同数据源之间动态切换
*/
@Primary
@Bean
public DataSource dynamicDataSource(){

DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(Data1());
//配置多数据源
Map<Object,Object> dsMap = new HashMap<>();
dsMap.put("data1",Data1());
dsMap.put("data2",Data2());

dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}

/**
* 配置@Transactional注解事务
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}

}
自定义切面

切面实现方法通过注解中的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
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.wqy.data.annotation.DataSource)")
public void pointcutConfig(){

}
@Before("pointcutConfig()")
public void before(JoinPoint joinPoint){
//获得当前访问的class
Class<?> className = joinPoint.getTarget().getClass();
//获得访问的方法名
String methodName = joinPoint.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)joinPoint.getSignature()).getParameterTypes();

String dataSource = null;
try {
// 得到访问的方法对象
Method method = className.getMethod(methodName, argClass);
// 判断是否存在@DataSource注解
if (method.isAnnotationPresent(DataSource.class)) {
DataSource annotation = method.getAnnotation(DataSource.class);
// 取出注解中的数据源名
dataSource = annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
// 设置数据源key
DataSourceContextHolder.setDataSource(dataSource);
}

@After("pointcutConfig()")
public void after(JoinPoint joinPoint){
DataSourceContextHolder.clearDataSource();
}
}
使用注解

在方法上面用自定义的数据源注解声明数据源,就可以实现不同方法,不同数据源调用。

1
2
3
4
java复制代码    @DataSource("dataSource1")
public void queryUser() {
userMapper.select();
}

原理解析

通过AOP对方法进行切面,将注解中的value获取到,并设置为数据源Key,通过数据源配置类,拿到数据源对应的数据库bean,进而实现数据源切换。

AbstractRoutingDataSource

AbstractRoutingDataSource继承AbstractDataSource,如果声明一个类DynamicDataSource继承AbstractRoutingDataSource后,DynamicDataSource本身就相当于一种数据源。所以AbstractRoutingDataSource必然有getConnection()方法获取数据库连接。

大致流程为,通过determineCurrentLookupKey方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map<Object, DataSource>,里面应该保存当前所有可切换的数据源。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;}

本文转载自: 掘金

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

1…207208209…956

开发者博客

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