这里是weihubeats,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党
背景
测试线上的公参是通过skywalking
agent的方式进行传递的。
如果是本地则会因为获取不到公参报错影响正常测试。所以需要本地也进行公参传递
正常链路是 前端 -> 网关 -> 通过skywalking
进行公参处理全链路传递 -> 业务系统通过skywalking
侧的sdk(TraceContext
)进行公参获取
本地传递主要是两个问题
- 本地启动不想接入
skywalking
的agent启动 - 本地测试不走网关,公参不会存放在
skywalking
的上下文中
所以需要写一个简单的本地测试公参传递
目标
我们的目的很简单:
- 业务零侵入
- 能获取到公参
方案
业务本身获取公参有一个XiaoZouBaseRequest
工具类。然后会提供公参的方法获取
比如
public class XiaoZouBaseRequest implements Serializable {public String getDevice() {return TraceContext.getCorrelation(DEVICE).orElse("");}
}
我们如何做到业务无感知的实现公参获取呢?
其实很简单
我们写一个最简单的拦截器。将用户请求的请求头放入一个上下文中。然后对XiaoZouBaseRequest
这个类就行代理。从我们的上下文中获取公参,而不是从skywalking
实现
公参上下文
public class RequestContextHolder {private static final TransmittableThreadLocal<Map<String, String>> requestHeaders = new TransmittableThreadLocal<>();public static void setRequestHeaders(Map<String, String> headers) {requestHeaders.set(headers);}public static Map<String, String> getRequestHeaders() {return requestHeaders.get();}public static void clear() {requestHeaders.remove();}}
这里上下文的存储我们不使用ThreadLocal
和InheritableThreadLocal
.
使用阿里transmittable-thread-local
提供的TransmittableThreadLocal
可以解决线程池上线文丢失问题
需要添加依赖
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.4</version>
</dependency>
实际如果为了简单方便也可以直接使用
ThreadLocal
web拦截器
拦截器很简单实现HandlerInterceptor
即可
@Component
@Profile("dev")
public class LocalRequestHeaderInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Enumeration<String> headerNames = request.getHeaderNames();Map<String, String> headers = new HashMap<>();while (headerNames.hasMoreElements()) {String key = headerNames.nextElement();String value = request.getHeader(key);headers.put(key, value);}RequestContextHolder.setRequestHeaders(headers);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {RequestContextHolder.clear();}
}
- 使用
@Profile
保证只在dev生效 - 如果是sdk的方式
@Component
是没用的,我们需要通过其他方式让该bean注入spring中
拦截器配置
@Configuration
@Profile("dev")
public class LocalRequestMvcConfigurer implements WebMvcConfigurer {@Autowiredprivate LocalRequestHeaderInterceptor interceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(interceptor);}
}
字节码增强XiaoZouBaseRequest
@Component
@Profile("dev")
public class XiaoZouBaseRequestEnhancer implements BeanFactoryPostProcessor {private static final String GET_DEVICE = "getDevice";private static final String GET_DEVICE_BODY = "{\n" +" return com.xiaozou.common.interceptor.RequestContextHolder.getRequestHeaders().get(com.xiaozou.common.CommonParameters.DEVICE);\n" +" }";private void enhanceBaseRequest() {try {ClassPool pool = ClassPool.getDefault();pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));CtClass cc = pool.get("com.xiaozou.common.XiaoZouBaseRequest");enhanceMethod(cc, GET_DEVICE, GET_DEVICE_BODY);cc.toClass();cc.detach();} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {enhanceBaseRequest();}private void enhanceMethod(CtClass cc, String methodName, String methodBody) throws Exception {cc.getDeclaredMethod(methodName).setBody(methodBody);}}
这里有比较多的小细节
- 增强
XiaoZouBaseRequestEnhancer
需要在XiaoZouBaseRequestEnhancer
加载之前。因为如果一个类已经被类加载器加载,将无法再次修改它的字节码。所以这里使用BeanFactoryPostProcessor
来扩展 - 在字节码增强中使用的类和方法都必须使用全类名,不然会报错
java.lang.RuntimeException: javassist.CannotCompileException: [source error] no such class: RequestContextHolder
测试
写一个参数DTO
public class TestDTO extends XiaoZouBaseRequest {
}
一个测试controller
@GetMapping("/test")public String test(TestDTO testDTO) {System.out.println("device = " + testDTO.getDevice());}
能够正常获取到公参就说明没问题