外观
上下文与异步任务
Base 用 MVC 拦截器建立请求 TraceId,用 SessionUserProvider 连接身份来源,并在默认执行器中传递请求属性和 MDC。三者都是运行时上下文,不代替身份验证,也不是持久化的任务消息。
关联一次请求
TraceIdInterceptorConfig 为 MVC 路径 /** 注册拦截器,请求进入后读取名为 trace-id 的 Header:
| 输入 | 行为 |
|---|---|
| 非空白且不超过 36 字符 | 原样放入 MDC 的 traceId,不要求 UUID 格式 |
| 缺失、空白或超过 36 字符 | 生成新的 UUID 放入 MDC |
| 请求完成 | 在 afterCompletion 删除 MDC 的 traceId |
BaseResponseModel 创建时读取该 MDC 字段并存入响应,所以响应应在当前请求中创建。没有设置 MDC 时,字段可为空。该拦截器不会设置响应 Header,不自动向下游 HTTP 请求传递 Header,也不创建完整的分布式追踪 Span。
在应用日志格式中加入 %X{traceId},便于用同一值关联日志。一个简单的控制台格式示例:
yaml
logging:
pattern:
console: '%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n'这是应用日志格式配置,不是 Base 开关。请求头 TraceId 由调用方提供时只经过长度与空白检查,不能拿它作为用户身份、幂等键或可信业务标识。进入 MVC 前被 Filter 拦截的请求,以及自行创建的后台线程,不保证经过这一初始化过程。
读取会话身份
Base 定义 com.own.component.base.login.SessionUserProvider,由应用注册实现,或由已接入的 Authentication 组件提供适配。仅引入 Base 不会创建用户 Provider;不访问身份的接口可以正常使用其他 Base 能力。
| Provider 方法 | 契约与默认值 |
|---|---|
loginUser() | 必须实现,提供当前 BaseLoginUser,没有身份时可返回 null |
permissionUser() | 必须实现,提供当前 BasePermissionUser,没有身份时可返回 null |
loginUser(Long userId)、permissionUser(Long userId) | 可选的指定用户查询,默认返回空列表 |
isLoginVerification() | 默认 true;决定 Provider 路径下是否执行取用户时的登录校验 |
SessionUserProviderHolder 使用 ObjectProvider.getIfAvailable() 取得一个 Provider。多个实现并存时需有明确的优先候选;没有 Provider 时,首次走到 Provider 读取会抛出 IllegalStateException("请实现 SessionUserProvider 接口")。Holder 保存静态引用,不是每次请求重新寻找 Bean,也不适合用来动态切换多个应用上下文。
常用静态入口位于 com.own.component.base.login.util.SessionUserUtil:
| 方法 | 当前行为 |
|---|---|
getLoginUser() / getLoginUser(true) | 获取身份并按校验策略检查;未登录抛 U0009 |
getLoginUser(false) | 跳过登录检查,但仍可能因缺少 Provider 抛异常,不能视为“永不失败的匿名读取” |
getPermissionUser() / getPermissionUser(boolean) | 对应的权限身份读取 |
isLogin() | 调用 getLoginUser(false) 后检查对象非空且 isLogin() 为真;缺少 Provider 时也可能抛异常 |
checkLogin() | isLogin() 为假时抛 U0009 |
loginUser(userId) / permissionUser(userId) | 直接委托指定用户查询,Base 不增加用户访问范围校验 |
Provider 的 isLoginVerification=false 只跳过 getLoginUser(true) 等获取方法中的检查;它不生成身份,获取结果仍可能是 null,也不让显式 checkLogin() 自动通过。
Authentication 的默认适配器读取 own.security.login-verification,其默认值为 false,与 SessionUserProvider 接口的默认 true 不同;接入时按 Authentication 配置显式设置应用策略。
用户模型
BaseLoginUser 要求实现 token()、userId()、userName()、userType()。默认 client() 为 web、organization() 为 -、identity() 为 user,isLogin() 默认返回 true。创建该接口实现本身不会验证 Token,应用必须从可信认证结果构造身份,匿名实现应明确返回未登录状态。
默认 uuid() 每次调用产生新 UUID,不能当作稳定设备或会话 ID。默认 json() 直接拼接值且包含 Token,不保证正确引用或转义;对外只序列化应用选定的字段,不把该字符串作为标准身份协议。
BasePermissionUser 额外定义 roleNameList() 和 checkOperation(module, operations)。Base 不提供角色来源或操作权限判定实现;系统菜单业务可向认证组件提供角色与模块操作数据。
在当前请求覆盖身份
在认证已完成、身份可信的服务器代码中,可调用 SessionUserUtil.setLoginUser(loginUser)。它只写当前 Servlet request attribute,不修改 Provider,也不签发或校验 Token。非 Servlet 请求上下文调用会抛“当前请求上下文不存在,无法设置登录用户”。
覆盖身份优先于 Provider;读取权限身份时,仅当覆盖对象实现 BasePermissionUser 才优先使用它,否则仍访问 Provider。覆盖路径的登录校验不受 Provider 的 isLoginVerification=false 影响。
clearLoginUser() 删除当前请求的覆盖值;没有 Servlet 上下文时不执行操作。临时切换身份后应清理或恢复调用前已知的身份,注意清理不会自动恢复此前的另一层覆盖值。异步任务与请求可能持有同一 request 对象,不应在异步线程中修改这些属性来切换身份。
提交携带上下文的任务
注入 ExecutorUtil,通过 execute(Runnable)、execute(condition, Runnable) 或 submit(Callable<T>) 使用默认 customAsyncExecutor。条件为假时不提交任务;有返回值的任务取得 Future<T> 后可由调用方决定如何等待。
以下 Controller 放在快速开始同一个演示应用的扫描范围内,无需用户 Provider:
java
package com.example.demo;
import com.own.component.base.model.R;
import com.own.component.base.model.ResultModel;
import com.own.component.base.util.executor.ExecutorUtil;
import com.own.constant.ConstantCommon;
import org.slf4j.MDC;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@Profile("base-demo")
@RestController
public class BaseTaskDemoController {
private final ExecutorUtil executor;
public BaseTaskDemoController(ExecutorUtil executor) {
this.executor = executor;
}
@GetMapping("/base-demo/task-trace")
public ResultModel<String> taskTrace() {
var task = executor.submit(() -> MDC.get(ConstantCommon.TRACE_ID));
return R.success(ExecutorUtil.get(task, 2, TimeUnit.SECONDS));
}
}携带 trace-id: base-task-001 请求 GET /base-demo/task-trace,正常完成时 data 应为 base-task-001。该示例在 HTTP 线程等待任务完成,只用于核对任务上下文,并不是立即返回的后台作业接口。
ExecutorUtil.get(future) 一直等待;带超时的重载超时后抛 BusinessSimpleException("任务执行时间过长"),不会自动取消任务。中断时恢复当前线程中断标记后抛 RuntimeException,任务执行异常也包装为 RuntimeException。直接操作返回的 Future 可自行处理取消与异常。
默认资源设置
令 n 为 JVM 报告的可用处理器数,默认执行器设置如下。这些值来自 Java 配置,不是可直接绑定的 YAML 属性。
| 设置 | 默认值 |
|---|---|
| Bean 名 | customAsyncExecutor |
| 核心线程数 | n + 1 |
| 最大线程数 | 2 × (n + 1) |
| 队列容量 | 333 × (n + 1) |
| 线程名前缀 | cat- |
| 上下文装饰器 | ContextCopyingDecorator |
| 拒绝策略 | CallerRunsPolicy,饱和时可在提交线程执行 |
| 关闭设置 | waitForTasksToCompleteOnShutdown=true,代码没有指定等待终止时长 |
Base 没有自行启用 @EnableAsync,也不保证未指定执行器的 @Async 一定选择该池。应用启用 Spring 异步代理后,应显式使用 @Async("customAsyncExecutor");直接注入 ExecutorUtil 不需要 @EnableAsync。
上下文传递边界
装饰器在提交时取得 RequestAttributes 引用和 MDC 副本,运行时设置到执行线程,结束后调用 RequestContextHolder.resetRequestAttributes() 与 MDC.clear()。它不会复制请求正文、数据库事务、任意 ThreadLocal 或独立安全上下文。
这里共享的是请求对象引用,并不是可长期保存的身份快照。需要在请求完成后继续处理的任务,应提前提取所需用户 ID、业务参数与追踪信息,以独立数据传入任务;不要在延迟任务中继续读取已结束请求的流或属性。
默认拒绝策略可能在调用线程执行任务,而装饰器结束时只清理、不恢复原 MDC 和 RequestAttributes,饱和时会影响调用线程后续的上下文读取。若应用需要严格保持提交线程上下文,可替换拒绝策略或装饰器,并明确处理任务拒绝。
自定义执行器
提供同名 ThreadPoolTaskExecutor Bean 覆盖默认池。以下配置演示固定容量及直接拒绝策略,并仍保留 Base 的上下文装饰器:
java
package com.example.config;
import com.own.component.base.util.executor.ContextCopyingDecorator;
import com.own.component.base.util.executor.ExecutorConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration(proxyBeanMethods = false)
public class ApplicationExecutorConfiguration {
@Bean(name = ExecutorConfig.ASYNC_EXECUTOR_NAME)
public ThreadPoolTaskExecutor customAsyncExecutor() {
var executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("app-task-");
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(10);
return executor;
}
}由 Spring 完成初始化。队列满时会向提交方报告拒绝,调用方需按业务处理;这组容量仅是应用配置示例。只修改 spring.task.execution.* 不会改变 Base 工厂内硬编码的这些值。
默认 ExecutorUtil 按限定名注入 ThreadPoolTaskExecutor,不能只放一个同名的普通 ExecutorService 来替换;需要不同类型时同时替换调用方式及相关 Bean。
虚拟线程入口
ExecutorUtil.newVirtualThreadPerTaskExecutor() 每次创建独立的虚拟线程执行器,调用方负责关闭;VIRTUAL_THREAD_PER_TASK_EXECUTOR 则是类级共享实例,没有 Base 自动配置提供的关闭回调。
两者均直接调用 JDK 工厂,不应用 ContextCopyingDecorator,也没有默认池的队列容量限制。不要假定在虚拟线程中仍能读取请求身份、MDC 或事务;任务需要什么数据,就显式传入什么数据。它们也不是系统日志专用异步记录器。