吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 942|回复: 10
收起左侧

[经验求助] 当自定义的注解用在异步方法上时,请教Spring AOP切面技术拿不到请求头信息问题

[复制链接]
TabKey9 发表于 2024-4-2 15:09
50吾爱币

<!-- markdown -->
SpringBoot项目,项目中早已整合了一套Spring AOP切面相关技术实现,该有的流程都有,常规操作就是给方法加上自定义注解就可以在被调用时,根据当前请求头获取到用户信息(token)、请求IP地址等信息,然后分两种情况写入数据库日志表:

  • 1、正常请求日志,每次该方法被调用时都会被记录入库;
  • 2、当抛出异常时,额外记录一条异常日志信息(与1同表);

上面文字提到的内容是系统架构中目前已经实现的一整套相关业务的具体实现。但是...

但是现在有个新的需求(当自定义的注解用在异步方法上时,如何在Spring AOP切面恰当的位置获取到请求头里token等信息),当我尝试将该现有的日志注解写在主线程中的“某个异步方法”时,切面日志类在写入数据库之前有个“获取请求头”信息的的操作理所当然的报错了。

这两天我没少查阅资料,为了存请求头信息,考虑过Redis、MQTT、MySQL在记录一条请求头的记录等等,都被我推翻了。目前我采用写死的方案:

  • 1、我假设一个不存在的异步方法操作员的用户的信息,定义好这个用户信息Object的所有预设属性值。
  • 2、请求方式既不是GET也不是POST,由于写入数据库日志表的时候这个字段是String,我直接预设“Async”。
  • 3、还缺什么信息我全部都预设...

然后我将当前的日志切面类(LogAspect)抽离成父类(BaseLogAspect),再定义一个异常日志切面类(LogAsyncAspect),分别继承自父类(BaseLogAspect),其中LogAspect留空不写,完整继承父类;LogAsyncAspect则是部分继承,部分重写父类方法,这里我重写了获取用户请求头信息的那个方法,我不再通过String userStr = request.getHeader(AuthConstant.USER_TOKEN_HEADER);获取,而是改成用我上面预设的用户信息,请求ip用localhost;(以上还在撸代码,执行结果暂时未知是否符合当前预期)

/**
 * @apiNote 日志异步切面类<br>
 * 部分继承 BaseLogAspect 抽象类,部分重写。
 * @Since 2024年3月29日
 * @AuThor TabKey9
 * @version 0.0.1.240329
 */
@Slf4j
@Aspect
@Component
public class LogAsyncAspect extends BaseLogAspect {

    @Override
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            // 虚拟一个异步线程专用账号信息
            UserDto userDto = new UserDto();
            userDto.setId(-1L);
            userDto.setUsername("异步操作专员");
            // MySQL 日志记录表实体对象
            SysOperLogEntity operLog = new SysOperLogEntity();
            // 操作状态
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址 【获取不到】
            operLog.setOperIp("localhost");
            // 请求url 【获取不到】
            operLog.setOperUrl("/**");
            // 操作人员
            operLog.setOperName(userDto.getUsername());
            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(e.getMessage());
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod("Async");
            // 处理设置注解上的参数 【调用父类方法】
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 设置消耗时间 【TIME_THREADLOCAL在父类用protected修饰符定义】
            operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
            operLog.setOperTime(new Date());
            // 保存数据库 【sysOperLogService在父类用protected修饰符定义】
            sysOperLogService.save(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        } finally {
            //【TIME_THREADLOCAL在父类用protected修饰符定义】
            TIME_THREADLOCAL.remove();
        }
    }
}

<u>请问:我的做法合理吗?或者有哪里考虑不周吗?以及还有其它解决方案嘛?</u>

最佳答案

查看完整内容

private HttpServletRequest request;请求头、账号信息,一般都是和请求线程的threadLocal绑定的。新起的异步线程,要想拿到主线程的东西,考虑用InheritableThreadLocal代替threadlocal或者其他方式显示传递

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

hulikaimen 发表于 2024-4-2 15:09
private HttpServletRequest request;请求头、账号信息,一般都是和请求线程的threadLocal绑定的。新起的异步线程,要想拿到主线程的东西,考虑用InheritableThreadLocal代替threadlocal或者其他方式显示传递
kmkim 发表于 2024-4-2 15:51
zhuxiangyu1024 发表于 2024-4-2 16:32
你是不是漏了某些信息,首先要明确请求都到了系统了请求头肯定是存在的,只是没取到,你应该考虑为什么没取到的问题而不是直接不取了。比如是不是因为类被代{过}{滤}理的问题。sping是个相当成熟的项目,每天有无数个javaer在测试他,如果你没找到相关的问题,只能是你的问题找错方向了。
 楼主| TabKey9 发表于 2024-4-2 17:54
zhuxiangyu1024 发表于 2024-4-2 16:32
你是不是漏了某些信息,首先要明确请求都到了系统了请求头肯定是存在的,只是没取到,你应该考虑为什么没取 ...

@Autowired
private HttpServletRequest request;

@ApiOperation("demo-异步方法测试异常-主线程")
@GetMapping("/execAsyncTest")
public R execAsyncTest(@RequestParam Map<String, Object> params) {
    System.out.println("主线程正常执行");
    // 在主线程能正常拿到用户的请求头信息
    String userStr = request.getHeader(AuthConstant.USER_TOKEN_HEADER);
    // ...
    // 调用异步方法
    execAsync();
    // 调用这个接口的前端页面容易超时,改用异步,前端秒响应。
    return R.ok("正在处理中,请稍后查看处理情况");
}

@Async
@LogAsync(title = "在这个自定义注解(AOP切面相关)中拿不到请求头信息",
        businessType = BusinessType.OTHER,
        operatorType = OperatorType.MANAGE)
protected void execAsync() {
    System.out.println("异步线程正在执行");
    // 具体业务
    // ...
    // 在异步方法里,可能拿不到请求头信息吧。即使拿不到,我还可以从主线程拿到,再当参数传进来。
    // ...
    // 测试抛出异常
    throw new RuntimeException("异步方法测试异常");
}
zhuxiangyu1024 发表于 2024-4-2 22:43
TabKey9 发表于 2024-4-2 17:54
[md]@Autowired
    private HttpServletRequest request;

你这需求是记录异步方法的日志而不是在异步方法上面加注解吧,倒不如外面套一层普通的service,虽然奇怪了点,但是其他方法虽然可以写,但是涉及到线程可能更奇怪也有可能出现意想不到的错误。
 楼主| TabKey9 发表于 2024-4-3 09:52
zhuxiangyu1024 发表于 2024-4-2 22:43
你这需求是记录异步方法的日志而不是在异步方法上面加注解吧,倒不如外面套一层普通的service,虽然奇怪 ...

是的,了解越深入越觉得超纲了。我技术不够扎实,硬要把这需求实现起来不仅麻烦,还有一堆可能存在却还是捉摸不透的隐患。改用最传统的方式实现了,try/catch,调异步方法的时候,传入请求头信息,在catch的时候自己写(写一条错误日志到MySQL),或者在finally的时候,虽然简单粗bao,但自我感觉安全可控。
zhuxiangyu1024 发表于 2024-4-3 10:36
TabKey9 发表于 2024-4-3 09:52
是的,了解越深入越觉得超纲了。我技术不够扎实,硬要把这需求实现起来不仅麻烦,还有一堆可能存在却还是 ...

你这还不如套一层service在service里面加上日志注解,把异步的方法抽出来一个异步service,方法就直接调用异步的service方法,你现在这种做法把程序弄得很乱,整个系统的日志来源于一个日志注解,但是某个其他人根本不知道的异步的方法里又会自己往日志库里写日志。
 楼主| TabKey9 发表于 2024-4-3 19:26
zhuxiangyu1024 发表于 2024-4-3 10:36
你这还不如套一层service在service里面加上日志注解,把异步的方法抽出来一个异步service,方法就直接调用 ...

我也不想搞这么乱,但是前同事写的日志切面当时没考虑过异步情况下的应用场景,如今要加需求,我对切面技术不熟悉,感觉改不了,我只好曲线救国:需要参数尽量在主线程获取,传递下去,异步方法里捕获到异常之后,写库。

在那张数据表对应的service下定义2个方法:
    /**
     * @apiNote 获取系统操作日志实体(写异常日志)
     * @Param status 操作状态(0正常 1异常)
     */
    SysOperLogEntity getSysOperLogEntity(Integer status);

    /**
     * @apiNote 保存异步错误日志
     * @param sysOperLogEntity T
     * @param title 标题
     * @param businessType 业务类型(0其它 1新增 2修改 3删除)
     * @param operatorType 操作类别(0其它 1后台用户 2手机端用户 3异步操作)
     */
    void saveAsyncErrorLog(@NotNull SysOperLogEntity sysOperLogEntity, @NotBlank String title, @NotNull Integer businessType, @NotNull Integer operatorType);

其它“异步”的业务场景有需要时也都是调用这两个方法,前者在主线程调用,后者在捕获到异常时调用,相互配合完成这个需求。

回眸一看,有屎山代码那种感觉了
 楼主| TabKey9 发表于 2024-4-3 20:00
hulikaimen 发表于 2024-4-3 14:04
private HttpServletRequest request;请求头、账号信息,一般都是和请求线程的threadLocal绑定的。新起的异 ...

点赞,我在主线程存一个Redis的key(threadLocal.set("key**");),在子线程取(threadLocal.get();),及解决了传递数据问题,也解决了(一旦子线程被创建以后,再操作父线程中的ThreadLocal变量,那么子线程是不能感知的)的问题。不过我有2个疑问:

1、日志切面里有我想要取的数据,它跟我请求某个接口的控制层是处于同一个线程吗?
2、假设2台电脑同时登录同一个账号,请求同一个接口,请问此时threadLocal的值安全吗?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-16 05:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表