编程技巧篇之线程上下文

时间:2022-11-15 01:03:36


一、 背景

在实际开发过程中,有时候会采用抽象继承的方式,如模板模式、策略模式实现代码编排和复用。

存在的问题:

  • 不同流程之间会出现重复使用同一个参数调用下游接口的情况
  • 后面某个步骤依赖前面某个步骤产出的中间数据,但前面步骤的返回值中不关注这个中间数据

编程技巧篇之线程上下文


为了提高性能,避免重复的接口调用;为了降低耦合,可以使用线程上下文工具。


二、线程上下文

2.1 定义线程上下文

如果需要在多线程环境下使用,添加依赖

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.6.1</version>
</dependency>

线程上下文参考代码:

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 复用查询结果

定义模板抽象类

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();
}
}

}

定义实现类

@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 降低耦合

有时候后续环节需要前面步骤产出的一些上下文对象。

可以在该步骤中可以将该部分对象写入到上下文中,在后续执行环节从上下文中取出使用。

@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 )。

优点:上下文中有哪些对象和对象的类型一目了然。

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 找到设置和获取的代码。

@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 ,很少去探索写的编码姿势。

本文提供一种使用线程上下文来提高性能和降低耦合的方式,希望对大家有帮助。

如果文章对你有帮助,欢迎点赞、关注,我会持续创作更多作品。

编程技巧篇之线程上下文