一、start背景和简介
1.背景
工作中经常需要将多个springboot项目共同的非业务模块抽取出来,比如访问日志、维护请求上下文中的用户信息或者链路id等等。此次模拟的是请求中用户信息维护,方便整个请求中用户信息的取用。
2.作用
根据项目组的实际需求,封装starter,可以简化开发,统一规范的效果。
3.规范
官方的starter包规范:spring-boot-starter-xxx
自定义starter包规范:xxx-spring-boot-starter
二、stater定义
1.创建maven项目(trace-spring-boot-stater),并添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.su</groupId>
<artifactId>trace-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 引入 Spring Boot 统一版本父项目管理依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
2.UserInfo 定义请求上下文内容的类
@Data
public class UserInfo {
private String traceId;
private String spanId;
private String userNo;
}
3.UserContext 实现同一个或父子线程中上下文用户信息的取用
public class UserContext {
public static final TransmittableThreadLocal<UserInfo> userInfo = new TransmittableThreadLocal<>();
private UserContext() {
}
public static void set(UserInfo user) {
userInfo.set(user);
}
public static UserInfo get() {
return (UserInfo) userInfo.get();
}
public static void remove() {
userInfo.remove();
}
public static String userNo() {
UserInfo userInfo = (UserInfo) UserContext.userInfo.get();
return Objects.isNull(userInfo) ? null : userInfo.getUserNo();
}
}
4.UserContextInterceptor 拦截器,拦截请求中的信息并存储
@Slf4j
@Component
public class UserContextInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "traceId";
private static final String SPAN_ID = "spanId";
private static final String APP_ID = "appId";
private static final String USER_NO = "userNo";
@Resource
private AppIdsConfig appIdsConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 验证请求方的 AppId
String appId = request.getHeader(APP_ID);
Set<String> appIdsSet = appIdsConfig.getIdsSet();
if (StringUtils.isBlank(appId) || CollectionUtils.isEmpty(appIdsSet) || !appIdsSet.contains(appId)) {
returnJson(response, ComResponse.fail(ResponseCodeEnums.AUTHOR_ERROR_CODE));
return false;
}
String spanId = UUIDGenerator.getUUID();
String traceId = request.getHeader(TRACE_ID);
String userNo = request.getHeader(USER_NO);
if (StringUtils.isEmpty(traceId)) {
traceId = UUIDGenerator.getUUID();
}
UserInfo userInfo = new UserInfo();
userInfo.setUserNo(userNo);
userInfo.setTraceId(traceId);
userInfo.setSpanId(spanId);
UserContext.set(userInfo);
MDC.put(TRACE_ID, traceId);
MDC.put(SPAN_ID, spanId);
log.info("{requestServer:{},traceId:{},spanId:{}}", appId, traceId, spanId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.remove();
MDC.remove(TRACE_ID);
MDC.remove(SPAN_ID);
}
private void returnJson(HttpServletResponse response, ComResponse<Object> result) {
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
try (PrintWriter writer = response.getWriter()) {
writer.print(JacksonUtil.toJsonString(result));
} catch (IOException e) {
log.error("response error", e);
}
}
}
5.AppIdsConfig配置类,读取配置文件中的信息
注意:使用@ConfigurationProperties的好处是懒加载,如果配置文件中没有配置也不会报错。
如果使用@Value,如果配置文件中找不到,则会报错
@ConfigurationProperties(prefix = "app")
@Component
public class AppIdsConfig {
private String ids;
private Set<String> idsSet;
public String getIds() {
return ids;
}
public void setIds(String ids) {
this.ids = ids;
if(StringUtils.isNotEmpty(ids)){
this.idsSet = org.springframework.util.StringUtils.commaDelimitedListToSet(ids);
}else{
this.idsSet = Collections.emptySet();
}
}
public Set<String> getIdsSet() {
return idsSet;
}
}
6.配置拦截器
@Configuration
public class TraceConfiguration implements WebMvcConfigurer {
@Resource
private UserContextInterceptor userContextInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userContextInterceptor).addPathPatterns("/**")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/doc.html/**");
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("doc.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
}
}
7.最后重点,创建配置文件,把需要自动装载的类配置上
我用的springboot3以下版本,所以在resource/META-INF下新建一个spring.factories文件,添加配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.su.trace.configure.TraceConfiguration
8.maven打包 clean,install/deploy
三、引用stater
在其他项目是使用上边自定义的starter
1.添加依赖
<dependency>
<groupId>com.su</groupId>
<artifactId>trace-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.添加配置文件内容(如果多个项目的配置内容一致,可以考虑将配置信息放到公共配置里)
app.ids=activity,crm,order,product,customer
3.测试类
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("/trace")
public void testTrace(){
log.info("userID:{}", UserContext.userNo());
}
}
4.测试