一、 背景
在实际开发过程中,有时候会采用抽象继承的方式,如模板模式、策略模式实现代码编排和复用。
存在的问题:
- 不同流程之间会出现重复使用同一个参数调用下游接口的情况
- 后面某个步骤依赖前面某个步骤产出的中间数据,但前面步骤的返回值中不关注这个中间数据
- …
为了提高性能,避免重复的接口调用;为了降低耦合,可以使用线程上下文工具。
二、线程上下文
2.1 定义线程上下文
如果需要在多线程环境下使用,添加依赖
代码语言:javascript复制 <dependency>
<groupId>com.alibabagroupId>
<artifactId>transmittable-thread-localartifactId>
<version>2.6.1version>
dependency>
线程上下文参考代码:
代码语言:javascript复制import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;
/**
* 线程上下文
*/
public class ThreadContext {
private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>();
/**
* 初始化上下文
*/
protected static void initContext() {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
CONTEXT.set(new HashMap<>(8));
} else {
CONTEXT.get().clear();
}
}
/**
* 清除上下文
*/
protected static void clearContext() {
CONTEXT.remove();
}
/**
* 获取上下文内容
*/
public static <T> T getValue(String key) {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
return null;
}
return (T) con.get(key);
}
/**
* 设置上下文参数
*/
public static void putValue(String key, Object value) {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
CONTEXT.set(new HashMap<>(8));
con = CONTEXT.get();
}
con.put(key, value);
}
}
2.2 使用案例
2.2.1 复用查询结果
定义模板抽象类
代码语言:javascript复制public abstract class AbstractSomeService {
abstract void step1(SomeReq req);
abstract void step2(SomeReq req);
abstract void step3(SomeReq req);
//模板
public final void play(SomeReq req){
//初始化上下文
ThreadContext.initContext();
try{
// 步骤1
step1(req);
// 步骤2
step2(req);
// 步骤3
step3(req);
}finally{
// 清空上下文
ThreadContext.clearContext();
}
}
}
定义实现类
代码语言:javascript复制@Component
public class ASomeServiceImpl extends AbstractSomeService {
@Resource
private UserService userService;
@Override
public void step1(SomeReq req) {
// 查询用户信息
UserInfo userInfo = userService.query(req.getUserId());
ThreadContext.put("USER_INFO", userInfo)
// 其他步骤
}
@Override
public void step2(SomeReq req) {
// 直接获取初始化部分已经查询过的用户信息
UserInfo userInfo = ThreadContext.get("USER_INFO")
// 其他步骤
}
@Override
public void step3(SomeReq req) {
// 代码省略
}
}
假设 ASomeServiceImpl
的 step1 和 step2 方法中都需要查询用户信息,可以在 step1 中查询后放到线程上下文中,在 step2 中直接获取使用,避免再次执行 RPC 调用。
如果获取用户信息调用耗时 10 ms ,那么相当于降低了 10 ms ,如果还有其他查询结果可以复用,就可以降低更多耗时。
当然,有些接口可以使用这种方式,有些接口必须重新发起调用,需要根据实际情况来决定。
2.2.2 降低耦合
有时候后续环节需要前面步骤产出的一些上下文对象。
可以在该步骤中可以将该部分对象写入到上下文中,在后续执行环节从上下文中取出使用。
代码语言:javascript复制@Component
public class BSomeServiceImpl extends AbstractSomeService {
@Resource
private UserService userService;
@Override
public void step1(SomeReq req) {
// 代码省略
// 后续步骤需要的内容写入上下文
SomeDTO some = xxx;
ThreadContext.put("SOME_INFO", some)
// 代码省略
}
@Override
public void step2(SomeReq req) {
// 代码省略
}
@Override
public void step3(SomeReq req) {
// 代码省略
// 获取前面步骤的一些上下文
SomeDTO some = ThreadContext.get("SOME_INFO")
// 代码省略
}
}
2.3 看法
2.3.1 殊途同归
当然线程上下文并不是唯一的解法,你也可以定义上下文对象( SomeContext
)作为返回值。
将可复用的对象写入到自定义的上下文对象中,后续环节使用时直接从 SomeContext
中取出使用即可。
缺点:每个业务都需要自定义专用的上下文对象,甚至为了传递上下文需要修改函数签名(原本是 void 的返回值,会定义为 Context )。
优点:上下文中有哪些对象和对象的类型一目了然。
代码语言:javascript复制public abstract class AbstractSomeService {
abstract SomeContext step1(SomeReq req);
abstract void step2(SomeReq req, SomeContext context);
abstract void step3(SomeReq req, SomeContext context);
//模板
public final void play(SomeReq req){
// 步骤1
SomeContext context = step1(req);
// 步骤2
step2(req, context);
// 步骤3
step3(req, context);
}
}
2.3.2 可读性
使用线程上下文后,获取可复用对象和设置可复用对象的方法大概率不会放在一起。
建议使用 @see
或者 {@link }
的方式提供设置和获取方法之间的跳转快捷方式。
@Component
public class BSomeServiceImpl extends AbstractSomeService {
@Resource
private UserService userService;
/**
* 步骤1
*
* SOME_INFO 在 {@link BSomeServiceImpl#step3} 中使用
*/
@Override
public void step1(SomeReq req) {
// 代码省略
// 后续步骤需要的内容写入上下文
SomeDTO some = xxx;
ThreadContext.put("SOME_INFO", some)
// 代码省略
}
@Override
public void step2(SomeReq req) {
// 代码省略
}
/**
* 步骤3
*
* SOME_INFO 构造自 {@link BSomeServiceImpl#step1}
*/
@Override
public void step3(SomeReq req) {
// 代码省略
// 获取前面步骤的一些上下文
SomeDTO some = ThreadContext.get("SOME_INFO")
// 代码省略
}
}
此外,建议大家把上下文的 key 定义为常量,这样即使不使用上面的方式跳转,也可以很快通过常量 find usage 找到设置和获取的代码。
代码语言:javascript复制@Component
public class BSomeServiceImpl extends AbstractSomeService {
@Resource
private UserService userService;
/**
* 步骤1
*
* SOME_INFO 在 {@link BSomeServiceImpl#step3} 中使用
*/
@Override
public void step1(SomeReq req) {
// 代码省略
// 后续步骤需要的内容写入上下文
SomeDTO some = xxx;
ThreadContext.put(ContextConstant.SOME_INFO, some)
// 代码省略
}
@Override
public void step2(SomeReq req) {
// 代码省略
}
/**
* 步骤3
*
* SOME_INFO 构造自 {@link BSomeServiceImpl#step1}
*/
@Override
public void step3(SomeReq req) {
// 代码省略
// 获取前面步骤的一些上下文
SomeDTO some = ThreadContext.get(ContextConstant.SOME_INFO)
// 代码省略
}
}
三、总结
很多同学写惯了 CURD ,很少去探索写的编码姿势。
本文提供一种使用线程上下文来提高性能和降低耦合的方式,希望对大家有帮助。