特别说明:通常,我们会将SpringBoot
写作SpringBoot
,而此处写作spring-boot
是因项目的确就叫做spring-boot-x
。以下简称项目。
项目地址:https://github.com/OpeningO/spring-boot-x
源起
娱坛有云一人一首成名曲,虽互联网技术与娱之文艺不同,但个人仍以为技术与艺类同,皆为高雅之事。故成此项目,是以个人有利器,便于而后开疆扩土。
总览
更新说明
- 支持分布式锁、幂等处理 [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
的异常处理机制,可以将原来的错误信息中插入其他信息、或将其解析或转换为其他信息;
-
如SpringBoot
之starter
动态装配或在yml
中配置相关特性;
-
简化的Redis
操作;
-
提炼Elasticsearch
之HighlevelClient
常用操作;
-
feign
的请求头参数的处理:合并上下游的请求头参数,并发场景的数据处理策略;
-
基于Druid
和Hikari
的动态路由RoutingDataSource
;
-
SpringBoot
应用的配置信息的自动拷贝;
-
更多继续完善… …
-
引入依赖
1
2
3
4
5
|
<dependency>
<groupId>org.openingo.boot</groupId>
<artifactId>spring-boot-x</artifactId>
<version>${spring-boot-x.version}</version>
</dependency>
|
细说特性
分布式锁
基于redis实现
1
2
3
4
5
6
7
8
|
final Lock a = DistributedLock.newLock("a");
try {
final boolean b = a.tryLock();
Assert.isTrue(b, "get lock error");
return "ok => " + b;
} finally {
a.unlock();
}
|
幂等处理
提供了两个注解
1
2
3
4
5
|
@Idempotent(keyEl = "#userEntity.id", expireMinutes = 1)
@PostMapping("/idempotent")
public String hello(@RequestBody UserEntity userEntity) {
...
}
|
手动事务管理更新 [2021.8.16] since 5.1.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Autowired
private TransactionTemplateX transactionTemplateX;
private UserEntity saveUser(UserEntity user) {
return userRepo.save(user);
}
private void saveWithEx(UserEntity user) {
saveUser(user);
throw new RuntimeException("error");
}
public void saveEx(UserEntity user) {
transactionTemplateX.txRun(() -> this.saveWithEx(user));
}
public void saveExe(UserEntity user) {
UserEntity ret = transactionTemplateX.txCall(() -> {
return this.saveUser(user);
});
ret ...
}
|
分布式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
1
2
3
4
5
6
7
8
9
10
11
12
|
public class UseSafety {
private final Safety safety = new Safety();
public void action() {
this.safety.safetyRun(() -> ...);
}
public <T> T call() {
return this.safety.safetyCall(() -> ...);
}
}
|
请求日志
先看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
****************************************************************
:: SpringApplicationX :: for current request report information
****************************************************************
Client IP : 172.15.11.240
Request Time : 2021-01-09T10:03:43.202
Controller : orgg.openingo.x.controller.EsRestClientXController.(EsRestClientXController.java:1)
URI : http://localhost:18080/esx/recommend?q=%E5%8C%97
Handler(Action) : recommends
Method : GET
Processing Time : 0.0s
Header(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"]
Body : "北"
UrlQuery : q=%E5%8C%97
Parameter(s) : q=北,
Exception : [qicz] ElasticsearchStatusException[Elasticsearch exception [type=index_not_found_exception, reason=no such index [qicz]]]
----------------------------------------------------------------
|
以上请求是从Postman发起的一个请求,对此请求的各类参数都进行了一一罗列:
- 源(Client IP);
- 发起时间(Request Time);
- 请求的handler(对应的Controller、Action);
- 请求方式(Method);
- 处理时间(Processing Time);
- 请求头(Header);
- 请求体(Body);
- 参数(Parameters);
- 此次请求出现的异常(如没有异常此项自然就没有了)。
如何使用?
请求响应参数的自动装配(映射)
通常情况下,我们会将后端处理的结果按照固定的格式包装之后,再返回给前端,所以在Controller
需要对处理结果进行统一的包装处理,于是有了下面的这种代码:
1
2
3
4
5
|
@GetMapping("/resp")
public RespData resp() {
Object data = service....
return RespData.success(data);
}
|
这种统一的包装处理,既然是统一行为,那么必然是可以进行自动的统一的处理方式。于是有了这个请求响应参数的自动装配(映射)
。先看看使用了这个特性之后,我们的代码变成了什么样:
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
|
/**
* AutoRespController
*
* @author Qicz
*/
@RestController
@RequestMapping("/auto")
@AutoMappingRespResult
public class AutoRespController {
@GetMapping("/int")
public Integer toInt() {
return 123;
}
@GetMapping("/void")
public void toVoid() {
}
@GetMapping("/exception")
public void toException() {
throw new ServiceException("异常了");
}
}
|
可以看到,我们添加了一个@AutoMappingRespResult
注解,在方法体,并无什么特别的了。
如何使用?
- 引入项目依赖,在启动类上加入
@EnableExtension
;
- 在需要包装的
Controller
上加入@AutoMappingRespResult
即可。
特别说明
使用@AutoMappingRespResult
后都将使用org.openingo.jdkits.http.RespData
进行数据包装。而org.openingo.jdkits.http.RespData
是可以对返回的数据进行动态配置的。默认情况下,返回sc
、sm
、data
,如果需要可以使用org.openingo.jdkits.http.RespData.Config
修改它们为其他任意值。
嵌入SpringBoot的异常处理机制
SpringBoot
的错误处理是借助org.springframework.boot.web.servlet.error.DefaultErrorAttributes
进行的,常看到的信息如下:
1
2
3
4
5
6
7
|
{
"timestamp": "2020-07-13T05:49:06.071+0000",
"status": 500,
"error": "Internal Server Error",
"message": "testing exception",
"path": "/ex"
}
|
进行项目的嵌入异常处理后,可以得到以下的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"timestamp": "2020-07-13T05:49:06.071+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "org.openingo.spring.exception.ServiceException",
"message": "testing exception",
"path": "/ex",
"handler": "public java.util.Map org.openingo.x.controller.UserController.ex()",
"openingo.error": {
"ex": "org.openingo.spring.exception.ServiceException: testing exception",
"em": "testing exception",
"error": "Internal Server Error",
"ec": "ERROR_CODE"
}
}
|
如何使用?
扩展异常处理
默认情况下项目使用org.openingo.spring.http.request.error.DefaultServiceErrorAttributes
提供异常嵌入处理,可以扩展其对异常进行拓展处理,如下:
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
|
/**
* BusinessErrorAttributes
*
* @author Qicz
*/
@Component
public class BusinessErrorAttributes extends DefaultServiceErrorAttributes {
/**
* Decorate exception error code, custom for your business logic.
* <code>
* <pre>
* public Object decorateExceptionCode(Exception exception) {
* if (exception instanceof IndexOutOfBoundsException) {
* return 123;
* }
* return super.decorateExceptionCode(exception);
* }
* </pre>
* </code>
*
* @param exception the exception that got thrown during handler execution
*/
@Override
public Object decorateExceptionCode(Exception exception) {
if (exception instanceof IndexOutOfBoundsException) {
return 123;
}
if (exception instanceof JsonProcessingException) {
return 345;
}
if (exception instanceof MethodArgumentNotValidException
|| exception instanceof ConstraintViolationException
|| exception instanceof BindException
|| exception instanceof HttpMessageNotReadableException
|| exception instanceof MissingServletRequestPartException
|| exception instanceof MissingServletRequestParameterException
|| exception instanceof MultipartException) {
return 1234;
}
return super.decorateExceptionCode(exception);
}
/**
* Decorate error attributes, add extension attributes etc.
* the {@code errorAttributes} that has exception, handler, message,
* error, timestamp, status, path params.
*
* @param errorAttributes error attributes
* @param serviceErrorAttributes service error attributes
*/
@Override
public void decorateErrorAttributes(Map<String, Object> errorAttributes, Map<String, Object> serviceErrorAttributes) {
super.decorateErrorAttributes(errorAttributes, serviceErrorAttributes);
//serviceErrorAttributes.putAll(errorAttributes);
}
}
|
简化的Redis操作
默认情况下对redis
的操作需要使用redisTemplate
提供的繁琐操作,各种opsFor...
,使用简化方式之后业务代码中会将此类统统干掉,而且将大部分的操作都按照redis官方的command的方式进行了对齐,更容易理解和使用它们。先来看看使用的效果:
1
2
3
4
5
6
7
8
9
10
|
@GetMapping("/save")
public String save() {
try {
KeyNamingKit.set("openingo");
stringKeyRedisTemplateX.set("name", "Qicz");
return "ok";
} finally {
KeyNamingKit.remove();
}
}
|
在业务中,通常我们会对写入redis
的数据进行region的处理,也就是从key
的角度对数据进行划分,以上看到的KeyNamingKit
就是进行了这样的操作。当然了,仅仅使用KeyNamingKit
是不能完成这个功能的,还需要定义一个keyNamingPolicy
,当然项目已提供了一个默认的keyNamingPolicy
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
|
/**
* DefaultKeyNamingPolicy
*
* @author Qicz
*/
public class DefaultKeyNamingPolicy implements IKeyNamingPolicy {
/**
* if {@code KeyNamingKit.getNaming()} is "null" return key,
* otherwise return {@code KeyNamingKit.getNaming()}+{@code KeyNamingKit.NAMING_SEPARATOR}+key
* @param key
* @return wrapper key
*/
@Override
public String getKeyName(String key) {
String naming = KeyNamingKit.get();
if (ValidateKit.isNull(naming)) {
return key;
}
if (!naming.endsWith(KeyNamingKit.NAMING_SEPARATOR)) {
naming = naming + KeyNamingKit.NAMING_SEPARATOR;
}
return naming + key;
}
}
|
如何使用?
自定义keyNamingPolicy
按照如下即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/**
* KeyNamingPolicy
*
* @author Qicz
*/
@Data
public class KeyNamingPolicy implements IKeyNamingPolicy {
@Override
public String getKeyName(String key) {
String s = KeyNamingKit.get();
if (ValidateKit.isNotNull(s)) {
return s + ":qicz:" + key;
}
return "qicz:" + key;
}
}
|
在操作前使用KeyNamingKit.set
将当前操作的region
写入,如此处的openingo
1
2
3
4
5
6
7
8
9
10
|
@GetMapping("/save")
public String save() {
try {
KeyNamingKit.set("openingo");
stringKeyRedisTemplateX.set("name", "Qicz");
return "ok";
} finally {
KeyNamingKit.remove();
}
}
|
Elasticsearch之HighlevelClient常用操作
常用操作即
- saveOrUpdate类
- delete类
- search类:分页处理,随机推荐等等
- 同步处理、异步处理
如下:
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
|
/**
* EsRestClientXController
*
* @author Qicz
*/
@RestController
@RequestMapping("/esx")
public class EsRestClientXController {
@Autowired
RestHighLevelClientX restHighLevelClientX;
@SneakyThrows
@GetMapping("/recommend")
public List<Map> recommends(String q) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
// 如果该属性中有多个关键字 则都高亮
highlightBuilder.requireFieldMatch(true);
highlightBuilder.field("name");
highlightBuilder.field("addr");
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
q = "*" + q + "*";
boolQueryBuilder.must(QueryBuilders.wildcardQuery("name", q));
boolQueryBuilder.should(QueryBuilders.wildcardQuery("addr", q));
return this.restHighLevelClientX.randomRecommend(Map.class, "qicz", 3, boolQueryBuilder, highlightBuilder);
}
@GetMapping("/add")
public String addIndex() {
try {
MappingsProperties mappingsProperties = MappingsProperties.me();
mappingsProperties.add(MappingsProperty.me().name("id").type("long"));
mappingsProperties.add(MappingsProperty.me().name("name").textType().analyzer("ik_smart"));
restHighLevelClientX.createIndex("aaabc", null, mappingsProperties);
} catch (IOException e) {
e.printStackTrace();
}
return "ok";
}
@PostMapping("/put")
public String putDoc(@RequestBody Map user) {
boolean ret = false;
IndexRequest indexRequest = new IndexRequest("qicz");
indexRequest.id(user.get("id").toString());
try {
indexRequest.source(JacksonKit.toJson(user), XContentType.JSON);
ret = this.restHighLevelClientX.saveOrUpdate(indexRequest);
} catch (IOException e) {
e.printStackTrace();
}
return ret ? indexRequest.id() : null;
}
@GetMapping("/user/{id}")
public Map findOneById(@PathVariable("id") Integer id) {
Map<String, Object> ret = null;
try {
ret = this.restHighLevelClientX.findAsMapById("qicz", id.toString());
} catch (IOException e) {
e.printStackTrace();
}
return ret;
}
@DeleteMapping("/user/{id}")
public boolean deleteById(@PathVariable("id") Integer id) {
boolean ret = false;
try {
ret = this.restHighLevelClientX.deleteByDocId("qicz", id.toString());
} catch (IOException e) {
e.printStackTrace();
}
return ret;
}
@GetMapping("/user/find")
public Page<?> find(String q) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//q = "q+-&&||!(){}[]^\"~*?:\\";
q = QueryParser.escape(q);
boolQueryBuilder.should(QueryBuilders.queryStringQuery(q));
sourceBuilder.query(boolQueryBuilder);
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.requireFieldMatch(true); //如果该属性中有多个关键字 则都高亮
highlightBuilder.field("docType");
highlightBuilder.field("docTitle");
highlightBuilder.field("docContent");
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
sourceBuilder.from(1);
sourceBuilder.size(10);
sourceBuilder.highlighter(highlightBuilder);
try {
return this.restHighLevelClientX.searchForPage(Object.class, "qicz", sourceBuilder, 1, 10);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
|
如何使用?
- 引入项目依赖及
spring-boot-starter-data-elasticsearch
及elasticsearch-rest-high-level-client
,在启动类上加入@EnableExtension
,即可使用。
基于Druid和Hikari的动态路由RoutingDataSource
spring官方提供了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
用于支持动态路由,但其对数据源本身的处理很少,故基于此对其从根上进行了扩展,于是有了org.openingo.spring.datasource.routing.RoutingDataSource
,可以便捷的加入移除关闭数据源,且支持Druid
及Hikari
。
如何使用?
SpringBoot应用的配置信息的自动拷贝
通常情况下,我们的SpringBoot
应用都会有各种的配置文件或yaml
或properties
,而在项目部署时有需要将他们进行外部化,便于动态配置,项目的此特性就基于此而实现。
如何使用?
若仅拷贝资源,使用java -jar xx.jar ccp
即可。
总结
以上对项目的现有的核心功能进行了简单说明,具体可查看项目源码或待后续在针对性的详细说明实现思路。
此项目在设计之初,经过了多番反复的调整,才有了现在的模样,在打磨过程中,对SpringBoot
又进一步的加深的了解,提升了许多。接下来,会继续将常见的功能不断的完善和加入。