ThreadLocal
是 Java 中的一个类,它提供了一种线程局部变量的机制。线程局部变量是指只能被同一个线程访问和修改的变量,不同线程之间互不干扰。ThreadLocal
可以用来解决多线程并发访问共享变量的问题。
ThreadLocal可以用来实现数据缓存,即在一个线程中缓存一些上下文相关的数据,以便在该线程的后续操作中使用。在使用ThreadLocal实现上下文缓存时,可以将需要缓存的数据存储在ThreadLocal对象中,然后在需要使用这些数据的地方,通过ThreadLocal对象获取数据。由于每个线程都有自己的ThreadLocal对象,因此不同线程之间的数据不会相互干扰,从而保证了线程安全性。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssetBranchCache {
private UnmodifiableMap<Integer, String> idToNameMap;
}
BranchContext 类定义了一个静态的 ThreadLocal
变量 BRANCH_CACHE_THREAD_LOCAL
,用于存储当前线程的 AssetBranchCache
对象:
public class BranchContext {
private static final ThreadLocal<@Nullable AssetBranchCache> BRANCH_CACHE_THREAD_LOCAL = new TransmittableThreadLocal<>();
/**
* 设置 assetBranchCache
*/
public static void load(@Nullable AssetBranchCache assetBranchCache) {
BRANCH_CACHE_THREAD_LOCAL.set(assetBranchCache);
}
/**
* 获取 assetBranchCache
*/
@Nullable
public static AssetBranchCache save() {
return BRANCH_CACHE_THREAD_LOCAL.get();
}
/**
* 清除 assetBranchCache 信息
*/
public static void remove() {
BRANCH_CACHE_THREAD_LOCAL.remove();
}
}
在使用ThreadLocal时需要注意内存泄漏问题,因为ThreadLocal对象是存储在每个线程的ThreadLocalMap中的,如果不及时清除ThreadLocal对象,可能会导致内存泄漏。
@Data
@Configuration
@CustomLog
public class IncidentInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors( @NotNull InterceptorRegistry registry) {
registry.addInterceptor(
new HandlerInterceptorAdapter() {
// preHandle方法:在请求处理前会执行
@Override
public boolean preHandle( @NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
BranchContext.remove();
return true;
}
// afterCompletion方法:在请求处理后会执行
@Override
public void afterCompletion(@NotNull HttpServletRequest request,@NotNull HttpServletResponse response, @NotNull Object handler,@Nullable Exception ex) throws Exception {
BranchContext.remove();
}
}
);
}
}
该类实现WebMvcConfigurer接口,用于配置拦截器。在addInterceptors方法中,注册了一个HandlerInterceptorAdapter类型的拦截器,该拦截器在请求处理前和请求处理后都会执行。在preHandle方法中,调用了BranchContext类的remove方法,用于清除ThreadLocal 缓存的数据。在afterCompletion方法中也调用了BranchContext类的remove方法,确保在请求处理完成后也清除当前线程的ThreadLocal 缓存的数据。
使用拦截器可以保证ThreadLocal中缓存的数据是实时更新的,每次请求进来都会清除缓存中的数据,重新加载。
为了在项目的各个地方获取ThreadLocal中缓存的资产组数据,可以定义一个线程上下文资产组数据持有者类 SaasThreadContextDataHolderBranch
public interface SaasThreadContextDataHolder {
}
@Data
@AllArgsConstructor
public class SaasThreadContextDataHolderBranch implements SaasThreadContextDataHolder {
@Nullable
private final AssetBranchCache assetBranchCache;
}
SaasThreadContextHolder 接口中定义了一些方法来管理 SaasThreadContextDataHolder 对象
public interface SaasThreadContextHolder<T extends SaasThreadContextDataHolder> {
// 获取 SaasThreadContextDataHolder 类
@NotNull
Class<T> getSaasThreadContextDataHolderClass();
// 尝试加载SaasThreadContextDataHolder
default boolean tryLoad(@NotNull SaasThreadContextDataHolder holder) {
if (!getSaasThreadContextDataHolderClass().isInstance(holder)) {
return false;
}
this.load((T) holder);
return true;
}
// 加载 SaasThreadContextDataHolder
void load(@NotNull T holder);
// 存储 SaasThreadContextDataHolder
@NotNull
T save();
// 清理 SaasThreadContextDataHolder
void remove();
}
// 应用程序中自动发现和注册该组件
@AutoService(SaasThreadContextHolder.class)
public class SaasThreadContextHolderBranch implements SaasThreadContextHolder<SaasThreadContextDataHolderBranch> {
@NotNull
@Override
public Class<SaasThreadContextDataHolderBranch> getSaasThreadContextDataHolderClass() {
return SaasThreadContextDataHolderBranch.class;
}
// 加载 SaasThreadContextDataHolderBranch 设置 ThreadLocal 缓存数据
@Override
public void load(@NotNull SaasThreadContextDataHolderBranch holder) {
AssetBranchCache assetBranchCache = holder.getAssetBranchCache();
if (assetBranchCache != null) {
BranchContext.load(assetBranchCache);
}
}
// 获取 ThreadLocal 缓存数据填充 SaasThreadContextDataHolderBranch
@NotNull
@Override
public SaasThreadContextDataHolderBranch save() {
return new SaasThreadContextDataHolderBranch(BranchContext.save());
}
// 清除 ThreadLocal 缓存数据
@Override
public void remove() {
BranchContext.remove();
}
}
public class SaasThreadContextUtil {
// 获取Spring容器中所有实现了SaasThreadContextHolder接口的组件
@NotNull
static List<SaasThreadContextHolder<?>> getSaasThreadContextHolders() {
return (List) IterableUtils.toList(
ServiceLoader.load(SaasThreadContextHolder.class)
);
}
@NotNull
public static List<SaasThreadContextDataHolder> save() {
List<SaasThreadContextHolder<?>> saasThreadContextHolders = getSaasThreadContextHolders();
List<SaasThreadContextDataHolder> saasThreadContextDataHolders = new ArrayList<>(
saasThreadContextHolders.size()
);
for (SaasThreadContextHolder<?> saasThreadContextHolder : saasThreadContextHolders) {
saasThreadContextDataHolders.add(saasThreadContextHolder.save());
}
return saasThreadContextDataHolders;
}
public static void load(
@NotNull List<SaasThreadContextDataHolder> saasThreadContextDataHolders
) {
for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {
for (SaasThreadContextDataHolder saasThreadContextDataHolder : saasThreadContextDataHolders) {
if (saasThreadContextHolder.tryLoad(saasThreadContextDataHolder)) {
break;
}
}
}
}
public static void remove() {
for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {
saasThreadContextHolder.remove();
}
}
}
@Component
@CustomLog
public class BranchNameCache {
@Getter
@Setter(onMethod_ = @Autowired)
private IBranchService branchService;
@Setter(onMethod_ = @Autowired)
private ApplicationContext applicationContext;
// 查询缓存数据 AssetBranchCache
public AssetBranchCache getAssetBranchCache() {
// 从ThreadLocal对象中获取缓存数据,如果不为null,直接返回
AssetBranchCache result = BranchContext.save();
if (result != null) {
return result;
}
// 查询缓存数据
BranchNameCache bean = applicationContext.getBean(BranchNameCache.class);
String cacheKey = String.format(RedisKey.ASSET_BRANCH, Objects.requireNonNull(TenantInfoContext.getTenantInfo()).getTenantId())+ CacheUtil.getCacheUserRegion();
result = bean.getAssetBranchInfo(cacheKey);
// 将缓存数据设置到ThreadLocal对象中
BranchContext.load(result);
return result;
}
// Spring Cache 注解
@Cacheable(value = "assetBranchCache", key = "#cacheKey")
public AssetBranchCache getAssetBranchInfo(String cacheKey) {
List<Branch> branches = this.getBranchService().listAll();
Map<Integer, String> idToNameMap = new HashMap<>(branches.size());
branches.forEach(branch -> {
idToNameMap.put(branch.getId().intValue(), branch.getName());
});
return new AssetBranchCache( (UnmodifiableMap<Integer, String>) UnmodifiableMap.unmodifiableMap(idToNameMap));
}
public Map<Integer, String> getBranchIdToNameMap() {
return this.getAssetBranchCache().getIdToNameMap();
}
}
① getAssetBranchCache方法:获取 AssetBranchCache数据,先从ThreadLocal对象中查询,如果查询结果不为null,直接返回,否则调用资产服务查询,最后将查询结果设置到ThreadLocal对象中。
② @Cacheable注解:Spring框架的注解,用于缓存方法的返回值。具体来说,@Cacheable
注解表示该方法的返回值应该被缓存,value
属性指定了缓存的名称,key
属性指定了缓存的键值,即用于查找缓存的唯一标识符。在这个例子中,缓存的名称为"assetBranchCache",缓存的键值为cacheKey
,cacheKey
是一个方法参数或者是一个表达式,用于生成缓存的键值。如果缓存中已经存在相同的键值,则直接返回缓存中的值,否则执行方法并将返回值存入缓存中。
@Api("安全事件信息")
@CustomLog
@Validated
@ResponseResult
@RestController
@RequestMapping("/api/v1/incidents")
public class IncidentController {
@CheckValidateAble
@PreAuthorize("hasAnyAuthority('superAdmin','incidentQuery')")
@PostMapping("/delayloaddata")
@ApiOperation("安全事件额外数据延迟加载")
@OperateLog(handle = { LoggerEnum.operation }, target = "operate.incident.log", action = "operate.incident.delayloaddata.log")
public IncidentDelayLoadData delayLoadData(
@RequestBody @Validated IncidentDelayLoadDataQo qo
) {
Map<String, IncidentDelayLoadData.EachIncidentDelayLoadData> incidentMap = delayLoadService.richInfo(qo);
return IncidentDelayLoadData.builder().incidentMap(incidentMap).build();
}
}
@Data
@Validated
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadDataQo {
@ApiModelProperty(value = "数据类型", example = "INCIDENT")
RiskTypeEnum riskType = RiskTypeEnum.INCIDENT;
@Valid
@Size(max = 1000, message = "延迟加载的数据最大1000条")
@NotNull
private List<SecurityEntry> data;
}
① 风险类型:安全事件,安全告警,风险资产
public enum RiskTypeEnum {
/**
* 安全事件
*/
INCIDENT,
/**
* 安全告警
*/
ALERT,
MOCK,
/**
* 风险资产
*/
RISK_ASSET;
}
② 事件延迟加载数据:
@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class SecurityEntry {
// 安全事件id、安全告警id、风险主机id
@NotBlank
@Pattern(regexp = "[^\"'&<>()+%\\\\]+")
private String uuId;
@Nullable
private Long assetId;
@Nullable
private Long lastTime;
@Nullable
private List<@NotBlank @Pattern(regexp = "[^\"'&<>()+%\\\\]+") String> alertIds;
}
@Nullable
注解是一种用于 Java 代码中的注解,它用于标记一个方法的返回值、参数或字段可以为 null。
③ 请求参数示例:
{
"data": [
{
"uuId": "alert-ad41da97-94b2-4091-9a6d-a6972a7ce919",
"assetId": 17582,
"lastTime": 1690460941
},
{
"uuId": "alert-a6a2d31f-34df-4176-bca5-1ee368e41006",
"assetId": 17581,
"lastTime": 1690460896
}
],
"riskType": "ALERT"
}
@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadData {
@ApiModelProperty("事件延迟加载数据")
private Map<String, EachIncidentDelayLoadData> incidentMap;
@Data
@Builder
@ApiModel(description = "每条事件的延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public static class EachIncidentDelayLoadData {
@ApiModelProperty("一键遏制禁用标记")
private Boolean oneClickDisposeDisabled;
private String oneClickDisposeStatus;
private String newestUsername;
private String checkOutUsername;
private String responsible;
private AlertSeverityNumber alertSeverityNumber;
private Long remarkNumber;
private Integer connectStatus;
@ApiModelProperty("当前资产类别信息")
private MagnitudeInfo magnitude;
@ApiModelProperty("处置入口威胁根除判断")
private TableCellDataVo entryDisposal;
@ApiModelProperty("主机IP")
private HostIpDelayVo hostIp;
@ApiModelProperty("主机资产组")
private HostBranchDelayVo hostBranchId;
@ApiModelProperty("主机业务组")
private HostGroupDelayVo hostGroupIds;
@ApiModelProperty("目的IP")
private SrcDstIpDelayVo dstIp;
@ApiModelProperty("源IP")
private SrcDstIpDelayVo srcIp;
@ApiModelProperty("源IP")
private AssetUserDelayVo assetUser;
}
}
① 外部类 IncidentDelayLoadData:包含一个名为incidentMap
的Map
对象,其中键为事件ID,值为EachIncidentDelayLoadData
对象,表示每个事件的延迟加载数据。
@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadData {
@ApiModelProperty("事件延迟加载数据")
private Map<String, EachIncidentDelayLoadData> incidentMap;
// ....
}
② 静态内部类 EachIncidentDelayLoadData:包含了事件的各种属性,如一键遏制禁用标记、最新用户名、检出用户名、责任人、告警严重性等等。其中,MagnitudeInfo
、TableCellDataVo
、HostIpDelayVo
、HostBranchDelayVo
、HostGroupDelayVo
、SrcDstIpDelayVo
和AssetUserDelayVo
都是其他自定义类的对象,用于表示不同的属性。
③ 静态内部类的使用:
Java中的静态内部类是指在一个类的内部定义的静态类。静态内部类与非静态内部类的区别在于,静态内部类不依赖于外部类的实例,可以直接通过外部类名访问,而非静态内部类必须依赖于外部类的实例才能访问。
静态内部类可以访问外部类的静态成员和方法,但不能访问外部类的非静态成员和方法。静态内部类也可以定义静态成员和方法,这些静态成员和方法与外部类的静态成员和方法类似,可以直接通过类名访问。
public class OuterClass {
// 外部类的成员和方法
public static class StaticInnerClass {
// 静态内部类的成员和方法
}
}
静态内部类的实例化方式如下:
OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
④ 响应数据示例:
{
"strCode": null,
"message": "成功",
"data": {
"incidentMap": {
"lqlapp35test3-452799db-2557-424b-ba86-aa1f2d72eb3e": {
"oneClickDisposeDisabled": true,
"oneClickDisposeStatus": "WAIT_DEAL",
"newestUsername": null,
"checkOutUsername": null,
"responsible": null,
"alertSeverityNumber": null,
"remarkNumber": 0,
"connectStatus": null,
"magnitude": null,
"entryDisposal": {
"originalValue": true,
"renderValue": null
},
"hostIp": {
"originalValue": "6.6.6.7",
"renderValue": "6.6.6.7(美国)"
},
"hostBranchId": {
"originalValue": 0,
"renderValue": ""
},
"hostGroupIds": {
"count": 0,
"data": []
},
"dstIp": null,
"srcIp": null,
"assetUser": null
},
"16c28c0d-649a-dispose-entity111-5d419e8790129": {
"oneClickDisposeDisabled": false,
"oneClickDisposeStatus": "WAIT_DEAL",
"newestUsername": null,
"checkOutUsername": null,
"responsible": null,
"alertSeverityNumber": null,
"remarkNumber": 0,
"connectStatus": null,
"magnitude": null,
"entryDisposal": {
"originalValue": true,
"renderValue": null
},
"hostIp": {
"originalValue": "192.168.40.29",
"renderValue": "192.168.40.29(管理IP范围)"
},
"hostBranchId": {
"originalValue": 1,
"renderValue": ""
},
"hostGroupIds": {
"count": 0,
"data": []
},
"dstIp": null,
"srcIp": null,
"assetUser": null
}
}
},
"code": 0
}
利用线程池管理并发任务的执行。通过将任务提交到线程池中,让线程池自动分配线程来执行任务,从而实现并发执行。线程池还可以控制并发任务的数量,避免系统资源被过度占用,从而提高系统的稳定性和可靠性。
@CustomLog
@Service
public class EventDelayLoadServiceImpl implements IEventDelayLoadService, ApplicationListener<ContextStoppedEvent> {
// 定义一个线程池
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
5,
20,
4,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder().setNameFormat(IncidentResponseDispositionServiceImpl.class.getSimpleName() + "-pool-%d")
.setDaemon(true).build(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
// 监听应用程序上下文停止事件,用于关闭线程资源
@Override
public void onApplicationEvent(@NotNull ContextStoppedEvent ignored) {
try {
THREAD_POOL_EXECUTOR.shutdown();
} catch (Exception e) {
log.error("停止线程池失败", e);
}
}
@Override
public Map<String, IncidentDelayLoadData.EachIncidentDelayLoadData> richInfo(IncidentDelayLoadDataQo delayLoadDataQo) {
List<SecurityEntry> securityEntryList = delayLoadDataQo.getData();
if (CollectionUtils.isEmpty(securityEntryList)) {
return Collections.emptyMap();
}
RiskTypeEnum riskType = delayLoadDataQo.getRiskType();
ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data = new ConcurrentHashMap<>(securityEntryList.size());
securityEntryList.forEach(entry -> data.put(entry.getUuId(), new IncidentDelayLoadData.EachIncidentDelayLoadData()));
List<Callable<Void>> callables;
// 根据riskType的不同,调用不同的方法获取Callable列表callables
switch (riskType) {
case INCIDENT:
callables = getIncidentCallableList(securityEntryList, data);
break;
case ALERT:
callables = getAlertCallableList(securityEntryList, data);
break;
case RISK_ASSET:
callables = getRiskAssetCallableList(securityEntryList, data);
break;
default:
return Collections.emptyMap();
}
// 通过线程池执行callables中的任务,并将结果存储在ConcurrentHashMap类型的数据data中
try {
List<Future<Void>> futures = THREAD_POOL_EXECUTOR.invokeAll(callables, 1, TimeUnit.MINUTES);
for (Future<Void> future : futures) {
try {
future.get();
} catch (Exception e) {
log.warn("incident delay data,future get warn", e);
}
}
} catch (Exception e) {
log.warn("incident delay data warn ", e);
}
return data;
}
}
以获取安全告警页面延迟加载数据的线程执行任务为例:getAlertCallableList
private List<Callable<Void>> getAlertCallableList(List<SecurityEntry> securityEntryList, ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data) {
HashMap<@NotNull String, @Nullable Long> uuIdToAssetId = new HashMap<>(securityEntryList.size());
for (SecurityEntry securityEntry : securityEntryList) {
uuIdToAssetId.put(securityEntry.getUuId(), securityEntry.getAssetId());
}
try {
List<Alert> alerts = alertDao.getAlerts(securityEntryList.stream().map(SecurityEntry::getUuId).collect(Collectors.toList()));
if (CollectionUtils.isEmpty(alerts)) {
log.warn("no alert data to deal");
data.clear();
return List.of();
}
// 尝试加载ThreadLocal对象中的缓存数据
List<SaasThreadContextDataHolder> contextDataHolders = SaasThreadContextUtil.save();
Callable<Void> assetCallable = getAssetCallable(securityEntryList, data, uuIdToAssetId, contextDataHolders);
Callable<Void> assetUserCallable = getAssetUserCallable(securityEntryList, data, uuIdToAssetId, contextDataHolders);
Callable<Void> assetBranchCallable = getAlertAssetBranchCallable(securityEntryList, alerts, data, contextDataHolders);
Callable<Void> remarkNumberCallable = getRemarkNumberCallable(securityEntryList, ALERT, data, contextDataHolders);
return Arrays.asList(
assetCallable,
assetUserCallable,
assetBranchCallable,
remarkNumberCallable
);
} catch (IOException | JsonSerializeException e) {
throw new IncidentRuntimeException(I18nUtils.i18n(I18nConstant.AlertOperateConstant.EXCEPTION_TO_BE_TRANSFERRED), e);
}
}
在该方法中定义了一个获取安全告警资产组信息线程体,继续看下 getAlertAssetBranchCallable 方法:
@CustomLog
@Service
public class EventDelayLoadServiceImpl implements IEventDelayLoadService, ApplicationListener<ContextStoppedEvent> {
@Setter(onMethod_ = { @Autowired })
private BranchNameCache branchNameCache;
private Callable<Void> getAssetBranchCallable(
List<SecurityEntry> securityEntryList, List<Incident> incidentList, ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data,
List<SaasThreadContextDataHolder> contextDataHolders
) {
return () -> {
try {
// 设置ThreadLocal对象中的缓存数据
SaasThreadContextUtil.load(contextDataHolders);
// BranchNameCache 获取 AssetBranchCache 数据
Map<Integer, String> branchIdToNameMap = branchNameCache.getBranchIdToNameMap();
for (SecurityEntry securityEntry : securityEntryList) {
//设置响应数据
eachIncidentDelayLoadData.setHostBranchId(
new HostBranchDelayVo(branchId, HostIpUtils.getHostBranch(assetId, branchId, branchIdToFullNameMap))
);
}
return null;
} finally {
// 清除ThreadLocal对象中的缓存数据
SaasThreadContextUtil.remove();
}
};
}
}