跳转到正文

上下文与异步任务

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()weborganization()-identity()userisLogin() 默认返回 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 或事务;任务需要什么数据,就显式传入什么数据。它们也不是系统日志专用异步记录器

示例涉及的源码类型

RResultModelConstantCommonExecutorConfig