Qicz’s Thoughts HUB

The creative and technical writing. Do more, challenge more, know more, be more.

细说spring-boot-x

特别说明:通常,我们会将SpringBoot写作SpringBoot,而此处写作spring-boot是因项目的确就叫做spring-boot-x。以下简称项目。

项目地址:https://github.com/OpeningO/spring-boot-x

源起

娱坛有云一人一首成名曲,虽互联网技术与娱之文艺不同,但个人仍以为技术与艺类同,皆为高雅之事。故成此项目,是以个人有利器,便于而后开疆扩土。

总览

maven

更新说明

  • 支持分布式锁、幂等处理 [2021.8.16更新] since v5.1.0
  • 重构整个包,发布4.0.0.RELEASE版本 [ 2021.6.30更新 ]
  • 优化分布式id生成器,隔离默认的redis配置及Zookeeper配置,让id生成与业务完全分离 [ 2021.6.30更新 ] since v3.2.0.RELEASE

特性清单

  • 手动事务管理 [2021.6.29更新]

  • 分布式id生成器gedid,DidLoader [ 2021.6.25更新 ]

  • Safety工具 [ 2021.6.25更新 ] [merge to jdkits since v5.1.0]

  • 请求日志,包括请求源、请求目标、请求参数、处理时间、错误异常等信息;

  • 请求响应参数的自动装配(映射);

  • 跨域的配置;

  • 嵌入SpringBoot的异常处理机制,可以将原来的错误信息中插入其他信息、或将其解析或转换为其他信息;

  • SpringBootstarter动态装配或在yml中配置相关特性;

  • 简化的Redis操作;

  • 提炼ElasticsearchHighlevelClient常用操作;

  • feign的请求头参数的处理:合并上下游的请求头参数,并发场景的数据处理策略;

  • 基于DruidHikari的动态路由RoutingDataSource

  • SpringBoot应用的配置信息的自动拷贝;

  • 更多继续完善… …

  • 引入依赖

    1<dependency>
    2    <groupId>org.openingo.boot</groupId>
    3    <artifactId>spring-boot-x</artifactId>
    4    <version>${spring-boot-x.version}</version>
    5</dependency>
    

细说特性

分布式锁

基于redis实现

1final Lock a = DistributedLock.newLock("a");
2try {
3  final boolean b = a.tryLock();
4  Assert.isTrue(b, "get lock error");
5  return "ok => " + b;
6} finally {
7  a.unlock();
8}

幂等处理

提供了两个注解

  • org.openingo.spring.boot.extension.idempotent.annotation.Idempotent 用于指定某方法是否需要幂等处理;

     1@Documented
     2@Target({ElementType.METHOD})
     3@Retention(RetentionPolicy.RUNTIME)
     4public @interface Idempotent {
     5  
     6  /**
     7   * the key spring el
     8   */
     9  String keyEl() default "";
    10  
    11  /**
    12   * expire minutes
    13   */
    14  long expireMinutes() default 5L;
    15}
    
  • org.openingo.spring.boot.extension.idempotent.annotation.IdempotentKey 用于从方法参数中指定幂等标识;

    1@Documented
    2@Target({ElementType.PARAMETER, ElementType.FIELD})
    3@Retention(RetentionPolicy.RUNTIME)
    4public @interface IdempotentKey {
    5}
    
  • 基于redis实现

1@Idempotent(keyEl = "#userEntity.id", expireMinutes = 1)
2@PostMapping("/idempotent")
3public String hello(@RequestBody UserEntity userEntity) {
4  ...
5}

手动事务管理更新 [2021.8.16] since 5.1.0

 1@Autowired
 2private TransactionTemplateX transactionTemplateX;
 3
 4private UserEntity saveUser(UserEntity user) {
 5  return userRepo.save(user);
 6}
 7
 8private void saveWithEx(UserEntity user) {
 9  saveUser(user);
10  throw new RuntimeException("error");
11}
12
13public void saveEx(UserEntity user) {
14  transactionTemplateX.txRun(() -> this.saveWithEx(user));
15}
16
17public void saveExe(UserEntity user) {
18  UserEntity ret = transactionTemplateX.txCall(() ->  {
19    return this.saveUser(user);
20  });
21  ret ...
22}

分布式Id生成器 [ 2021.6.25更新 ] since 3.0.0.RELEAE

从这里看分布式id使用说明

Safety 工具 [ 2021.6.25更新 ] since 3.0.0.RELEAE

已合并到jdkits,since v5.1.0

封装了ReentrantLock,具体查看源码org.openingo.spring.safety.Safety

 1public class UseSafety {
 2  
 3  private final Safety safety = new Safety();
 4	
 5  public void action() {
 6    this.safety.safetyRun(() -> ...);
 7  }
 8  
 9  public <T> T call() {
10    return this.safety.safetyCall(() -> ...);
11  }
12}

请求日志

先看看效果

 1****************************************************************
 2:: SpringApplicationX :: for current request report information 
 3****************************************************************
 4Client IP  : 172.15.11.240 
 5Request Time  : 2021-01-09T10:03:43.202 
 6Controller  : orgg.openingo.x.controller.EsRestClientXController.(EsRestClientXController.java:1)
 7URI  : http://localhost:18080/esx/recommend?q=%E5%8C%97 
 8Handler(Action)  : recommends
 9Method  : GET
10Processing Time  : 0.0s
11Header(s)  : [user-agent:"PostmanRuntime/7.26.5", accept:"*/*", cache-control:"no-cache", postman-token:"102da4af-27a4-4d04-bdcd-f8738c1dfc25", host:"localhost:18080", accept-encoding:"gzip, deflate, br", connection:"keep-alive"]
12Body  : "北"
13UrlQuery  : q=%E5%8C%97
14Parameter(s)  : q=北, 
15Exception  : [qicz] ElasticsearchStatusException[Elasticsearch exception [type=index_not_found_exception, reason=no such index [qicz]]]
16----------------------------------------------------------------

以上请求是从Postman发起的一个请求,对此请求的各类参数都进行了一一罗列:

  • 源(Client IP);
  • 发起时间(Request Time);
  • 请求的handler(对应的Controller、Action);
  • 请求方式(Method);
  • 处理时间(Processing Time);
  • 请求头(Header);
  • 请求体(Body);
  • 参数(Parameters);
  • 此次请求出现的异常(如没有异常此项自然就没有了)。

如何使用?

  • 引入项目依赖及spring-boot-starter-aop,在启动类上加入@EnableExtension即可;

     1/**
     2 * App
     3 *
     4 * @author Qicz
     5 */
     6@SpringBootApplication
     7@EnableExtension
     8public class App {
     9  
    10    public static void main(String[] args) throws InterruptedException {
    11        SpringApplicationX.run(App.class, args);
    12        SpringApplicationX.applicationInfo();
    13    }
    14}
    
  • 可配置?

    1openingo:
    2  http:
    3    request:
    4      cors:
    5        allowed-header: "*"
    6        enable: true
    7        allowed-all: true
    8      log:
    9        enable: true
    

    如上即可对log及cors进行配置。

请求响应参数的自动装配(映射)

通常情况下,我们会将后端处理的结果按照固定的格式包装之后,再返回给前端,所以在Controller需要对处理结果进行统一的包装处理,于是有了下面的这种代码:

1@GetMapping("/resp")
2public RespData resp() {
3    Object data = service....
4    return RespData.success(data);
5}

这种统一的包装处理,既然是统一行为,那么必然是可以进行自动的统一的处理方式。于是有了这个请求响应参数的自动装配(映射)。先看看使用了这个特性之后,我们的代码变成了什么样:

 1/**
 2 * AutoRespController
 3 *
 4 * @author Qicz
 5 */
 6@RestController
 7@RequestMapping("/auto")
 8@AutoMappingRespResult
 9public class AutoRespController {
10
11    @GetMapping("/int")
12    public Integer toInt() {
13        return 123;
14    }
15
16    @GetMapping("/void")
17    public void toVoid() {
18
19    }
20
21    @GetMapping("/exception")
22    public void toException() {
23        throw new ServiceException("异常了");
24    }
25}

可以看到,我们添加了一个@AutoMappingRespResult注解,在方法体,并无什么特别的了。

如何使用?

  • 引入项目依赖,在启动类上加入@EnableExtension
  • 在需要包装的Controller上加入@AutoMappingRespResult即可。

特别说明

使用@AutoMappingRespResult后都将使用org.openingo.jdkits.http.RespData进行数据包装。而org.openingo.jdkits.http.RespData是可以对返回的数据进行动态配置的。默认情况下,返回scsmdata,如果需要可以使用org.openingo.jdkits.http.RespData.Config修改它们为其他任意值。

嵌入SpringBoot的异常处理机制

SpringBoot的错误处理是借助org.springframework.boot.web.servlet.error.DefaultErrorAttributes进行的,常看到的信息如下:

1{
2    "timestamp": "2020-07-13T05:49:06.071+0000",
3    "status": 500,
4    "error": "Internal Server Error",
5    "message": "testing exception",
6    "path": "/ex"
7}

进行项目的嵌入异常处理后,可以得到以下的信息:

 1{
 2    "timestamp": "2020-07-13T05:49:06.071+0000",
 3    "status": 500,
 4    "error": "Internal Server Error",
 5    "exception": "org.openingo.spring.exception.ServiceException",
 6    "message": "testing exception",
 7    "path": "/ex",
 8    "handler": "public java.util.Map org.openingo.x.controller.UserController.ex()",
 9    "openingo.error": {
10        "ex": "org.openingo.spring.exception.ServiceException: testing exception",
11        "em": "testing exception",
12        "error": "Internal Server Error",
13        "ec": "ERROR_CODE"
14    }
15}

如何使用?

  • 引入项目依赖,在启动类上加入@EnableExtension,即可使用;

  • 可配置?

    1openingo:
    2  http:
    3      error:
    4        enable: true
    

    如上即可对error进行配置。

扩展异常处理

默认情况下项目使用org.openingo.spring.http.request.error.DefaultServiceErrorAttributes提供异常嵌入处理,可以扩展其对异常进行拓展处理,如下:

 1/**
 2 * BusinessErrorAttributes
 3 *
 4 * @author Qicz
 5 */
 6@Component
 7public class BusinessErrorAttributes extends DefaultServiceErrorAttributes {
 8
 9    /**
10     * Decorate exception error code, custom for your business logic.
11     * <code>
12     * <pre>
13     * public Object decorateExceptionCode(Exception exception) {
14     *    if (exception instanceof IndexOutOfBoundsException) {
15     *      return 123;
16     *    }
17     *   return super.decorateExceptionCode(exception);
18     * }
19     * </pre>
20     * </code>
21     *
22     * @param exception the exception that got thrown during handler execution
23     */
24    @Override
25    public Object decorateExceptionCode(Exception exception) {
26        if (exception instanceof IndexOutOfBoundsException) {
27            return 123;
28        }
29        if (exception instanceof JsonProcessingException) {
30            return 345;
31        }
32        if (exception instanceof MethodArgumentNotValidException
33                || exception instanceof ConstraintViolationException
34                || exception instanceof BindException
35                || exception instanceof HttpMessageNotReadableException
36                || exception instanceof MissingServletRequestPartException
37                || exception instanceof MissingServletRequestParameterException
38                || exception instanceof MultipartException) {
39            return 1234;
40        }
41        return super.decorateExceptionCode(exception);
42    }
43
44    /**
45     * Decorate error attributes, add extension attributes etc.
46     * the {@code errorAttributes} that has exception, handler, message,
47     * error, timestamp, status, path params.
48     *
49     * @param errorAttributes        error attributes
50     * @param serviceErrorAttributes service error attributes
51     */
52    @Override
53    public void decorateErrorAttributes(Map<String, Object> errorAttributes, Map<String, Object> serviceErrorAttributes) {
54        super.decorateErrorAttributes(errorAttributes, serviceErrorAttributes);
55        //serviceErrorAttributes.putAll(errorAttributes);
56    }
57}

简化的Redis操作

默认情况下对redis的操作需要使用redisTemplate提供的繁琐操作,各种opsFor...,使用简化方式之后业务代码中会将此类统统干掉,而且将大部分的操作都按照redis官方的command的方式进行了对齐,更容易理解和使用它们。先来看看使用的效果:

 1@GetMapping("/save")
 2public String save() {
 3    try {
 4        KeyNamingKit.set("openingo");
 5        stringKeyRedisTemplateX.set("name", "Qicz");
 6        return "ok";
 7    } finally {
 8        KeyNamingKit.remove();
 9    }
10}

在业务中,通常我们会对写入redis的数据进行region的处理,也就是从key的角度对数据进行划分,以上看到的KeyNamingKit就是进行了这样的操作。当然了,仅仅使用KeyNamingKit是不能完成这个功能的,还需要定义一个keyNamingPolicy,当然项目已提供了一个默认的keyNamingPolicy

 1/**
 2 * DefaultKeyNamingPolicy
 3 *
 4 * @author Qicz
 5 */
 6public class DefaultKeyNamingPolicy implements IKeyNamingPolicy {
 7
 8    /**
 9     * if {@code KeyNamingKit.getNaming()} is "null" return key,
10     * otherwise return {@code KeyNamingKit.getNaming()}+{@code KeyNamingKit.NAMING_SEPARATOR}+key
11     * @param key
12     * @return wrapper key
13     */
14    @Override
15    public String getKeyName(String key) {
16        String naming = KeyNamingKit.get();
17        if (ValidateKit.isNull(naming)) {
18            return key;
19        }
20        if (!naming.endsWith(KeyNamingKit.NAMING_SEPARATOR)) {
21            naming = naming + KeyNamingKit.NAMING_SEPARATOR;
22        }
23        return naming + key;
24    }
25}

如何使用?

  • 引入项目依赖及spring-boot-starter-data-redis,在启动类上加入@EnableExtension,即可使用;

  • 可配置?

    1openingo:
    2  redis:
    3    enable: true
    

自定义keyNamingPolicy

按照如下即可

 1/**
 2 * KeyNamingPolicy
 3 *
 4 * @author Qicz
 5 */
 6@Data
 7public class KeyNamingPolicy implements IKeyNamingPolicy {
 8
 9    @Override
10    public String getKeyName(String key) {
11        String s = KeyNamingKit.get();
12        if (ValidateKit.isNotNull(s)) {
13            return s + ":qicz:" + key;
14        }
15        return "qicz:" + key;
16    }
17}

在操作前使用KeyNamingKit.set将当前操作的region写入,如此处的openingo

 1@GetMapping("/save")
 2public String save() {
 3    try {
 4        KeyNamingKit.set("openingo");
 5        stringKeyRedisTemplateX.set("name", "Qicz");
 6        return "ok";
 7    } finally {
 8        KeyNamingKit.remove();
 9    }
10}

Elasticsearch之HighlevelClient常用操作

常用操作即

  • saveOrUpdate类
  • delete类
  • search类:分页处理,随机推荐等等
  • 同步处理、异步处理

如下:

  1/**
  2 * EsRestClientXController
  3 *
  4 * @author Qicz
  5 */
  6@RestController
  7@RequestMapping("/esx")
  8public class EsRestClientXController {
  9
 10    @Autowired
 11    RestHighLevelClientX restHighLevelClientX;
 12
 13    @SneakyThrows
 14    @GetMapping("/recommend")
 15    public List<Map> recommends(String q) {
 16        HighlightBuilder highlightBuilder = new HighlightBuilder();
 17        // 如果该属性中有多个关键字 则都高亮
 18        highlightBuilder.requireFieldMatch(true);
 19        highlightBuilder.field("name");
 20        highlightBuilder.field("addr");
 21        highlightBuilder.preTags("<span style='color:red'>");
 22        highlightBuilder.postTags("</span>");
 23
 24        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
 25
 26        q = "*" + q + "*";
 27        boolQueryBuilder.must(QueryBuilders.wildcardQuery("name", q));
 28        boolQueryBuilder.should(QueryBuilders.wildcardQuery("addr", q));
 29        return this.restHighLevelClientX.randomRecommend(Map.class, "qicz", 3, boolQueryBuilder, highlightBuilder);
 30    }
 31
 32    @GetMapping("/add")
 33    public String addIndex() {
 34        try {
 35            MappingsProperties mappingsProperties = MappingsProperties.me();
 36            mappingsProperties.add(MappingsProperty.me().name("id").type("long"));
 37            mappingsProperties.add(MappingsProperty.me().name("name").textType().analyzer("ik_smart"));
 38            restHighLevelClientX.createIndex("aaabc", null, mappingsProperties);
 39        } catch (IOException e) {
 40            e.printStackTrace();
 41        }
 42        return "ok";
 43    }
 44
 45    @PostMapping("/put")
 46    public String putDoc(@RequestBody Map user) {
 47        boolean ret = false;
 48        IndexRequest indexRequest = new IndexRequest("qicz");
 49        indexRequest.id(user.get("id").toString());
 50        try {
 51            indexRequest.source(JacksonKit.toJson(user), XContentType.JSON);
 52            ret = this.restHighLevelClientX.saveOrUpdate(indexRequest);
 53        } catch (IOException e) {
 54            e.printStackTrace();
 55        }
 56
 57        return ret ? indexRequest.id() : null;
 58    }
 59
 60    @GetMapping("/user/{id}")
 61    public Map findOneById(@PathVariable("id") Integer id) {
 62        Map<String, Object> ret = null;
 63        try {
 64            ret = this.restHighLevelClientX.findAsMapById("qicz", id.toString());
 65        } catch (IOException e) {
 66            e.printStackTrace();
 67        }
 68        return ret;
 69    }
 70
 71    @DeleteMapping("/user/{id}")
 72    public boolean deleteById(@PathVariable("id") Integer id) {
 73        boolean ret = false;
 74        try {
 75            ret = this.restHighLevelClientX.deleteByDocId("qicz", id.toString());
 76        } catch (IOException e) {
 77            e.printStackTrace();
 78        }
 79        return ret;
 80    }
 81
 82    @GetMapping("/user/find")
 83    public Page<?> find(String q) {
 84        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
 85
 86        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
 87        //q = "q+-&&||!(){}[]^\"~*?:\\";
 88        q = QueryParser.escape(q);
 89        boolQueryBuilder.should(QueryBuilders.queryStringQuery(q));
 90        sourceBuilder.query(boolQueryBuilder);
 91
 92        HighlightBuilder highlightBuilder = new HighlightBuilder();
 93        highlightBuilder.requireFieldMatch(true); //如果该属性中有多个关键字 则都高亮
 94        highlightBuilder.field("docType");
 95        highlightBuilder.field("docTitle");
 96        highlightBuilder.field("docContent");
 97        highlightBuilder.preTags("<span style='color:red'>");
 98        highlightBuilder.postTags("</span>");
 99
100        sourceBuilder.from(1);
101        sourceBuilder.size(10);
102        sourceBuilder.highlighter(highlightBuilder);
103        try {
104            return this.restHighLevelClientX.searchForPage(Object.class, "qicz", sourceBuilder, 1, 10);
105        } catch (IOException e) {
106            e.printStackTrace();
107        }
108        return null;
109    }
110}

如何使用?

  • 引入项目依赖及spring-boot-starter-data-elasticsearchelasticsearch-rest-high-level-client,在启动类上加入@EnableExtension,即可使用。

基于Druid和Hikari的动态路由RoutingDataSource

spring官方提供了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource用于支持动态路由,但其对数据源本身的处理很少,故基于此对其从根上进行了扩展,于是有了org.openingo.spring.datasource.routing.RoutingDataSource,可以便捷的加入移除关闭数据源,且支持DruidHikari

如何使用?

  • 引入项目依赖(使用druid时加入其依赖),在启动类上加入@EnableExtension,即可使用;

  • 示例:

     1/**
     2 * DataSourceService
     3 *
     4 * @author Qicz
     5 */
     6@Service
     7@Slf4j
     8public class DataSourceService implements IDataSourceService {
     9  
    10    @Autowired
    11    RoutingDataSource routingDataSource;
    12  
    13    @Autowired
    14    DruidDataSource dataSource;
    15  
    16    @Override
    17    public void switchDataSource(String name) throws SQLException {
    18        try {
    19            System.out.println("======before======"+name);
    20            routingDataSource.getConnection();
    21            System.out.println(routingDataSource.getCurrentUsingDataSourceProvider().hashCode());
    22            RoutingDataSourceHolder.setCurrentUsingDataSourceKey(name);
    23            routingDataSource.getConnection();
    24            System.out.println("======after======");
    25            System.out.println(routingDataSource.getCurrentUsingDataSourceProvider().hashCode());
    26        } finally {
    27            RoutingDataSourceHolder.clearCurrentUsingDataSourceKey();
    28        }
    29    }
    30  
    31    @Override
    32    public void add(String name) {
    33        //routingDataSource.setAutoCloseSameKeyDataSource(false);
    34        DruidDataSourceProvider druidDataSourceProvider = new DruidDataSourceProvider(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
    35        druidDataSourceProvider.startProviding();
    36        routingDataSource.addDataSource(name, druidDataSourceProvider);
    37    }
    38}
    

SpringBoot应用的配置信息的自动拷贝

通常情况下,我们的SpringBoot应用都会有各种的配置文件或yamlproperties,而在项目部署时有需要将他们进行外部化,便于动态配置,项目的此特性就基于此而实现。

如何使用?

  • 引入项目依赖;

  • 在启动类中使用SpringApplicationX,如下:

     1/**
     2 * App
     3 *
     4 * @author Qicz
     5 */
     6@SpringBootApplication
     7public class App {
     8  
     9    public static void main(String[] args) throws InterruptedException {
    10        SpringApplicationX.run(App.class, args);
    11        SpringApplicationX.applicationInfo();
    12    }
    13}
    

若仅拷贝资源,使用java -jar xx.jar ccp 即可。

总结

以上对项目的现有的核心功能进行了简单说明,具体可查看项目源码或待后续在针对性的详细说明实现思路。

此项目在设计之初,经过了多番反复的调整,才有了现在的模样,在打磨过程中,对SpringBoot又进一步的加深的了解,提升了许多。接下来,会继续将常见的功能不断的完善和加入。