这里是weihubeats ,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党
背景
测试线上的公参是通过skywalking
agent的方式进行传递的。
如果是本地则会因为获取不到公参报错影响正常测试。所以需要本地也进行公参传递
正常链路是 前端 -> 网关 -> 通过skywalking
进行公参处理全链路传递 -> 业务系统通过skywalking
侧的sdk(TraceContext
)进行公参获取
本地传递主要是两个问题
- 本地启动不想接入
skywalking
的agent启动 - 本地测试不走网关,公参不会存放在
skywalking
的上下文中
所以需要写一个简单的本地测试公参传递
目标
我们的目的很简单:
- 业务零侵入
- 能获取到公参
方案
业务本身获取公参有一个XiaoZouBaseRequest
工具类。然后会提供公参的方法获取
比如
java
public class XiaoZouBaseRequest implements Serializable {
public String getDevice() {
return TraceContext.getCorrelation(DEVICE).orElse("");
}
}
我们如何做到业务无感知的实现公参获取呢?
其实很简单
我们写一个最简单的拦截器。将用户请求的请求头放入一个上下文中。然后对XiaoZouBaseRequest
这个类就行代理。从我们的上下文中获取公参,而不是从skywalking
实现
公参上下文
java
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
可以解决线程池上线文丢失问题
需要添加依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.4</version>
</dependency>
实际如果为了简单方便也可以直接使用
ThreadLocal
web拦截器
拦截器很简单实现HandlerInterceptor
即可
java
@Component
@Profile("dev")
public class LocalRequestHeaderInterceptor implements HandlerInterceptor {
@Override
public 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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
RequestContextHolder.clear();
}
}
- 使用
@Profile
保证只在dev生效 - 如果是sdk的方式
@Component
是没用的,我们需要通过其他方式让该bean注入spring中
拦截器配置
java
@Configuration
@Profile("dev")
public class LocalRequestMvcConfigurer implements WebMvcConfigurer {
@Autowired
private LocalRequestHeaderInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor);
}
}
字节码增强XiaoZouBaseRequest
java
@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);
}
}
@Override
public 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
java
public class TestDTO extends XiaoZouBaseRequest {
}
一个测试controller
java
@GetMapping("/test")
public String test(TestDTO testDTO) {
System.out.println("device = " + testDTO.getDevice());
}
能够正常获取到公参就说明没问题