一、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";@Resourceprivate AppIdsConfig appIdsConfig;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 验证请求方的 AppIdString 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;}@Overridepublic 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 {@Resourceprivate UserContextInterceptor userContextInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userContextInterceptor).addPathPatterns("/**").excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/doc.html/**");}@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();ObjectMapper objectMapper = new ObjectMapper();objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);objectMapper.registerModule(new JavaTimeModule());}@Overridepublic 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.测试