能有幸点进来看,想必大家对ThreadLocal都有一定的了解,那么关于它究竟是个什么东东,我就不照本宣科了,在这篇文章里,我主要分享ThreadLocal相关的应用,作为开发2C项目的后端同学可能都需要考虑到一些公共参数的传递和使用,比如客户端IP、APK版本、渠道Code、用户账号等信息,最挫的方式就是在各DTO对象中定义这些属性,但聪明的你,坑定不会这么做,至少做一个公共的Base类:
public class PublicParam {
/**用户ID*/
private Long userId;
/**手机号*/
private String phone;
/**MAC*/
private String mac;
/**客户端SN序列号*/
private String sn;
/**IP*/
private String ip;
/**版本号*/
private String version;
/**客户端类型:IOS ANDROID*/
private String client;
}
然后再需要传递这些参数的时候使用 extands 继承一下,比如一个电商系统,创建购物车、订单DTO对象,示例就可以这样子写啦:
购物车DTO创建对象:
public class CreateCartDTO extends PublicParam {
/**产品ID*/
private Long productId;
/**产品skuID*/
private Long skuId;
/**购买数量*/
private Integer num = 1;
}
订单DTO创建对象:
public class CreateOrderDTO extends PublicParam{
/**产品ID*/
private Long productId;
/**产品skuID*/
private Long skuId;
/**购买数量 */
private Integer num = 1;
/**其它属性...*/
}
好的,这么做稍微会好一些,至少代码会少一点,然后我们就带上这些DTO参数对象开始在Controller、Service方法中穿梭,比如我要下单啦:
public boolean buyDirect(CreateOrderDTO orderDTO) {
// 保存订单
saveOrder(orderDTO.getUserId(), orderDTO.getProductId(), orderDTO.getSkuId());
// 扣减库存
ProductReduceDTO reduceDTO = new ProductReduceDTO(orderDTO.getProductId(), orderDTO.getSkuId(), orderDTO.getNum());
reduceStock(reduceDTO);
return false;
}
我们在保存订单和扣减库存操作后,尤其是失败后,一般需要日志打印一下结果,以便于线上问题定位等需要,比如想要把信息记录详细一些,如用户的IP,客户端版本version等。需要将参数reduceStock中将这些参数传递过去,或封装成一个新DTO对象,又或者直接是最开始的那个orderDTO对象,
boolean reduceStock(@RequestBody ProductReduceDTO reduceDTO) {
try {
log.info("-------- 用户下单扣减库存开始:");
log.info("-------- userId: {}, ip: {}, client: {}, version: {}",
reduceDTO.getUserId(), reduceDTO.getIp(),
reduceDTO.getClient(), reduceDTO.getVersion());
log.info("-------- 商品:{}, 总量减少:{}", reduceDTO.getProductId(), reduceDTO.getNum());
log.info("-------- SKU:{}, 减少:{}", reduceDTO.getProductId(), reduceDTO.getNum());
log.info("-------- 用户下单扣减库存成功!");
return true;
} catch (Exception e) {
log.error("userId: {}, ip:{}, client:{}, version:{}, reduceDTO: {} 扣减失败!",
reduceDTO.getUserId(),
reduceDTO.getIp(),
reduceDTO.getClient(),
reduceDTO.getVersion(),
JsonUtils.toJsonString(reduceDTO),
e);
return false;
}
}
当然实际的业务场景远比上面描述的复杂,或者有较长的调用链路,各个方法参数封装到新的DTO对象都冗余定义公共参数。
如果你也觉得不方便,那么可以继续往下面看了,嚯嚯。 基于Spring的Web应用,想必AOP的大家都不再陌生,在这主要使用AOP+ThreadLocal,便可实现公共参数的统一设置和回收,思想并不复杂, 由于我们并不知道系统中什么地方会用到这些公共参数,那么最好的办法就是都进行拦截设置, 定义公共参数存储对象定义ParamContext,用于存储ThreadLocal线程级的缓存信息:
public class ParamContext {
public static final String USER_ID = "userId";
public static final String IP = "ip";
public static final String VERSION = "version";
public static final String CLIENT = "client";
/**
* 参数缓存
*/
private static final ThreadLocal<HashMap<String, Object>> cache =
ThreadLocal.withInitial(HashMap::new);
/**
* 数据清理
*/
public static void clean() {
cache.remove();
}
public static Long getUserId() {
return null != cache.get().get(USER_ID) ? Long.valueOf(toString(cache.get().get(USER_ID))) : null;
}
public static void setUserId(String userId) {
if (!StringUtils.hasLength(userId)) return;
cache.get().put(USER_ID, Long.valueOf(userId));
}
// ...... 其它属性设置和获取类似,省略
private static String toString(Object o) {
if (null == o) {
return null;
}
return String.valueOf(o);
}
}
定义参数拦截AOP切面类:
@Aspect
@Component
public class PublicParamAspect {
//申明一个切点 里面是excution表达式
@Pointcut("execution(* com.ddoubuy..*Controller.*(..))")
private void paramPointCut() {
}
@Around(value = "paramPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
// 设置各参数到ThreadLocal的cache中
ParamContext.setUserId(request.getHeader(USER_ID));
ParamContext.setIp(request.getHeader(IP));
ParamContext.setClient(request.getHeader(CLIENT));
ParamContext.setVersion(request.getHeader(VERSION));
return joinPoint.proceed();
} finally {
// very import,必须调用clean方法进行ThreadLocal变量的remove
ParamContext.clean();
}
}
}
只需要在具体需要使用的地方,直接获取ParamContext缓存中设置的属性值即可,如上面的扣减库存日志,公共参数就不用通过reduceDTO传递啦:
boolean reduceStock(ProductReduceDTO reduceDTO) {
try {
log.info("-------- 用户下单扣减库存开始:");
log.info("-------- userId: {}, ip: {}, client: {}, version: {}",
ParamContext.getUserId(), ParamContext.getIp(),
ParamContext.getClient(), ParamContext.getVersion());
log.info("-------- 商品:{}, 总量减少:{}", reduceDTO.getProductId(), reduceDTO.getNum());
log.info("-------- SKU:{}, 减少:{}", reduceDTO.getProductId(), reduceDTO.getNum());
log.info("-------- 用户下单扣减库存成功!");
return true;
} catch (Exception e) {
log.error("userId: {}, ip:{}, client:{}, version:{}, reduceDTO: {} 扣减失败!",
ParamContext.getUserId(),
ParamContext.getIp(),
ParamContext.getClient(),
ParamContext.getVersion(),
JsonUtils.toJsonString(reduceDTO),
e);
return false;
}
}
扣减库存对应的执行结果正常打印,表明AOP拦截设置的缓存读取成功:
-------- 用户下单扣减库存开始:
-------- userId: 1, ip: 1.1.1.1, client: IOS, version: 1.0.1
-------- 商品:1, 总量减少:1
-------- SKU:1, 减少:1
-------- 用户下单扣减库存成功!
上面的demo只是简单示例,实际项目中用起来其实挺爽的,没试过的小伙伴可以实践一下,不过一定要注意做好ThreadLocal变量的remove,尽量避免所谓的内存泄漏问题。
那么,好了,本期的内容继续.....
到这儿,你可能就要吐槽了,单机版应用,没毛病,可以这么玩,但都2022年了,谁还不玩微服务、分布式,你这套行?比如微服务A调用微服务B,B调用C,后面的服务怎么使用这些公共变量呢?
为了装13,我花费了至少10根头发的代价,搭建了一套基于SpringCloud Alibaba微服务调用Demo来演示一波。 使用技术说明,实际工作中也是基于这一套技术,实践过的小伙伴得心应手了,没实践过的小伙伴了噶了噶
| 使用技术 | 简介 |
|---|---|
| SpringCloud Alibaba | 是阿里巴巴提供的微服务开发一站式解决方案,是阿里巴巴开源中间件与 Spring Cloud 体系的融合。 |
| Spring Cloud Gateway | 网关 |
| Nacos | 服务注册、发现和配置中心 |
| OpenFeign | Rest方式服务调用其它微服务应用,自带负载能力 |
贴一下项目的目录:
在gateway网关自定义一个全局过滤器RequestFilter,主要解析参数请求body体中的参数,并设置到request的header中,便于在参数在各个微服务组件中传递:
@Slf4j
@Configuration
public class RequestFilter implements GlobalFilter, Ordered {
private static final List<HttpMethod> bodyMethods = Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 无body体直接放行
if (!bodyMethods.contains(request.getMethod())) {
return chain.filter(exchange);
}
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
PublicParam publicParam = JsonUtils.parseObject(bodyString, PublicParam.class);
// 五*,设置公共参数到header中方便传输
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.addAll(request.getHeaders());
httpHeaders.add(PUBLIC_PARAM, JsonUtils.toJsonString(publicParam));
addHeader(httpHeaders, USER_ID, publicParam.getUserId());
addHeader(httpHeaders, IP, publicParam.getIp());
addHeader(httpHeaders, CLIENT, publicParam.getClient());
addHeader(httpHeaders, VERSION, publicParam.getVersion());
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(bytes);
return Mono.just(buffer);
});
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
if (bytes.length > 0) {
httpHeaders.setContentLength(bytes.length);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest)
.build());
});
}
@Override
public int getOrder() {
return Integer.MIN_VALUE + 1;
}
private void addHeader(HttpHeaders httpHeaders, String key, Object val) {
if (!StringUtils.hasLength(key) || null == val) {
return;
}
httpHeaders.add(key, String.valueOf(val));
}
}
我们使用OpenFeign提供的RequestInterceptor进行拦截,将需要的参数设置到Header中,传递给下游被调用的微服务:
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 所有的header都传递,建议根据自己的需求传递需要的参数
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 跳过 content-length,否则会报java.io.IOException: Incomplete output stream
if (name.equals("content-length")){
continue;
}
String values = request.getHeader(name);
requestTemplate.header(name, values);
}
}
}
}
然后在订单服务order-service中将扣减库存的拆分到商品服务product-service ,并通过OpenFeign方式进行调用:
商品服务 product-service中提供对应的PRC接口,主要就是是扣减库存对应的逻辑:
就这样,其它的不用做什么改动,我们就可以在各个微服务中传递这些公共参数,非常丝滑。 大家后面对ThreadLocal有什么高级应用和不同理解可以一起分享分享。