1.背景
工作中经常需要将多个springboot项目共同的非业务模块抽取出来,比如访问日志、维护请求上下文中的用户信息或者链路id等等。此次模拟的是请求中用户信息维护,方便整个请求中用户信息的取用。
2.作用
根据项目组的实际需求,封装starter,可以简化开发,统一规范的效果。
3.规范
官方的starter包规范:spring-boot-starter-xxx
自定义starter包规范:xxx-spring-boot-starter
1.创建maven项目(trace-spring-boot-stater),并添加依赖
- "1.0" encoding="UTF-8"?>
"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">
-
4.0.0 -
-
com.su -
trace-spring-boot-starter -
1.0-SNAPSHOT -
-
-
-
org.springframework.boot -
spring-boot-starter-parent -
2.3.4.RELEASE -
-
-
-
-
-
org.springframework.boot -
spring-boot-autoconfigure -
-
-
-
org.springframework.boot -
spring-boot-configuration-processor -
true -
-
-
-
org.projectlombok -
lombok -
1.18.24 -
-
-
com.alibaba -
transmittable-thread-local -
2.13.2 -
-
-
org.apache.commons -
commons-lang3 -
-
-
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 = 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
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 {
- 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
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
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
> 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
在其他项目是使用上边自定义的starter
1.添加依赖
-
-
com.su -
trace-spring-boot-starter -
1.0-SNAPSHOT -
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.测试
