SpringBoot ApplicationListener事件监听接口使用问题探究

 更新时间:2023年04月03日 09:33:10   作者:ForestSpringH  
这篇文章主要介绍了SpringBoot ApplicationListener事件监听接口使用问题,自定义监听器需要实现ApplicationListener接口,实现对应的方法来完成自己的业务逻辑。SpringBoot Application共支持6种事件监听

终日惶惶,不知归路;一日写起代码,突发奇想,若是在运行时发现自定义上下文的数据丢失,我们该如何解决处理数据丢失的问题?

问题复现一下,大家看下面的代码,观察是否有问题,又该如何解决这个问题:

@RequestMapping("verify")
@RestController
@DependsOn({"DingAppInfoService","CloudChatAppInfoService"})
public class LoginAction {
    @Qualifier("ElderSonService")
    @Autowired
    private ElderSonService elderSonService;
    @Qualifier("EmployeeService")
    @Autowired
    private EmployeeService employeeService;
    @Qualifier("UserThreadPoolTaskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor userThreadPoolTaskExecutor;
    private static AuthRequest ding_request = null;
    private static RongCloud cloud_chat = null;
    private static TokenResult register = null;
    private static final ThreadLocal<String> USER_TYPE = new ThreadLocal<>();
    /**
     * 注意不能在bean的生命周期方法上添注@CheckAppContext注解
     */
    @PostConstruct
    public void beforeVerifySetContext() {
        AppContext.fillLoginContext();
        Assert.hasText(AppContext.getAppLoginDingId(), "初始化app_login_ding_id错误");
        Assert.hasText(AppContext.getAppLoginDingSecret(), "初始化app_login_ding_secret错误");
        Assert.hasText(AppContext.getAppLoginReturnUrl(), "初始化app_login_return_url错误");
        Assert.hasText(AppContext.getCloudChatKey(), "初始化cloud_chat_key错误");
        Assert.hasText(AppContext.getCloudChatSecret(), "初始化cloud_chat_secret错误");
        if (!(StringUtils.hasText(AppContext.getCloudNetUri()) || StringUtils.hasText(AppContext.getCloudNetUriReserve()))) {
            throw new IllegalArgumentException("初始化cloud_net_uri与cloud_net_uri_reserve错误");
        }
        ding_request = new AuthDingTalkRequest(
                AuthConfig.builder().
                        clientId(AppContext.getAppLoginDingId()).
                        clientSecret(AppContext.getAppLoginDingSecret()).
                        redirectUri(AppContext.getAppLoginReturnUrl()).build());
        cloud_chat = RongCloud.getInstance(AppContext.getCloudChatKey(), AppContext.getCloudChatSecret());
    }
.....以下API方法无所影响......
}

其中可能令人不解的是controller组件里初始化方法的代码:

    public static void fillLoginContext() {
        DingAppInfo appInfo = SpringContextHolder.getBean(DingAppInfoService.class).findAppInfo(APP_CODE);
        setDingVerifyInfo(appInfo);
        CloudChatAppInfo cloudChatAppInfo = SpringContextHolder.getBean(CloudChatAppInfoService.class).findAppInfo(APP_CODE);
        setCloudChatInfo(cloudChatAppInfo);
    }
   public static void setDingVerifyInfo(DingAppInfo dingAppInfo){
        if (dingAppInfo.checkKeyWordIsNotNull(dingAppInfo)) {
            put(APP_LOGIN_DING_ID, dingAppInfo.getApp_id());
            put(APP_LOGIN_DING_SECRET, dingAppInfo.getApp_secret());
            put(APP_LOGIN_RETURN_URL, dingAppInfo.getApp_return_url());
        }
    }
    public static void setCloudChatInfo(CloudChatAppInfo cloudChatAppInfo){
        if (cloudChatAppInfo.checkKeyWordIsNotNull(cloudChatAppInfo)){
            put(CLOUD_CHAT_KEY,cloudChatAppInfo.getCloud_key());
            put(CLOUD_CHAT_SECRET,cloudChatAppInfo.getCloud_secret());
            put(CLOUD_NET_URI,cloudChatAppInfo.getCloud_net_uri());
            put(CLOUD_NET_URI_RESERVE,cloudChatAppInfo.getCloud_net_uri_reserve());
        }
    }

这里可以发现其实就是将一些项目定制的数据灌入我们的静态自定义上下文AppContext的本地线程ThreadLocal<Map<String,String>>对象中去,但是我们知道这个类型可是线程隔离的,不同的线程数据都不同,而我们的每一个请求都是一个线程,势必会导致数据的丢失,所以我们就算是在组件初始化时将数据给进去,下一个请求给进来也是会报出异常的。

解决思路(实际上不是这么解决的,但是也可以这么做,代价是性能耗费高):

设计一个监听者,一个发布者,在请求进入的方法上进行切面处理,切面检查AppContext对象数据,若为空则发布事件,不为空则进入方法:

事件原型:

public class AppContextStatusEvent extends ApplicationEvent {
    public AppContextStatusEvent(Object source) {
        super(source);
    }
    public AppContextStatusEvent(Object source, Clock clock) {
        super(source, clock);
    }
}

监听者:

@Component
public class AppContextListener implements ApplicationListener<AppContextStatusEvent> {
    @Override
    public void onApplicationEvent(AppContextStatusEvent event) {
        if ("FillAppContext".equals(event.getSource())) {
            AppContext.fillLoginContext();
        } else if ("CheckAppContextLogin".equals(event.getSource())) {
            boolean checkContext = AppContext.checkLoginContext();
            if (!checkContext) {
                AppContext.fillLoginContext();
            }
        }
    }
}

发布者(切面类):

@Aspect
@Component("AppContextAopAutoSetting")
public class AppContextAopAutoSetting {
    @Before("@annotation(com.lww.live.ApplicationListener.CheckAppContextLogin)")
    public void CheckContextIsNull(JoinPoint joinPoint){
        System.out.println("-----------aop---------CheckAppContextLogin---------start-----");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        boolean value = signature.getMethod().getAnnotation(CheckAppContextLogin.class).value();
        if (value){
            boolean checkContext = AppContext.checkLoginContext();
            if (!checkContext){
                SpringContextHolder.pushEvent(new AppContextStatusEvent("FillAppContext"));
            }
        }
    }
    @After("@annotation(com.lww.live.ApplicationListener.CheckAppContextLogin)")
    public void CheckContextIsNull(){
        System.out.println("-----------aop---------CheckAppContextLogin---------end-----");
        SpringContextHolder.pushEvent(new AppContextStatusEvent("CheckAppContextLogin"));
    }
}

那么AOP切面类捕获的是注解:

@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAppContextLogin {
    boolean value() default false;
    String info() default "";
}

这里不难发现我们在切面的前置与后置增强方法里都是先检查AppContext数据的完整性,再进行填充数据。这样如果我们每一个请求方法都打上注解@CheckAppContextLogin也可以实现,但是问题是除填充的方法外其他的数据太难维护且切面劫持代理的代价太高,检查数据的频率太高。

正确的解决方案:

根据数据的业务功能划分,因为主要是实现两个对象的填充,哪怕这几个数据丢失了,但是同一个controller组件的成员变量都是同一个对象,且都在初始化的时候进行了初始化,故后续切换请求了也不影响它们实现业务的能力:

 private static AuthRequest ding_request = null;
 private static RongCloud cloud_chat = null;

我们可以在拦截器中要求前端给我们传递当前用户的用户类型与唯一标识,来进行每一次请求的用户定制数据的封装(减少请求内调用方法链查库操作):

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = (String) request.getSession().getAttribute("token");
        String user_type = (String) request.getSession().getAttribute("user_type");
        if (StringUtils.hasText(token) && StringUtils.hasText(user_type)) {
            Context context = new Context();
            if (Objects.equals(user_type, "elder_son")) {
                ElderSon elderSon = elderSonService.getElderSonByElderSonId(token);
                context.setContextByElderSon(elderSon);
                return true;
            } else if (Objects.equals(user_type, "employee")) {
                Employee employee = employeeService.getEmployeeById(token);
                context.setContextByEmployee(employee);
                return true;
            }
        } else if (StringUtils.hasText(user_type)) {
            response.sendRedirect("/verify/login?user_type=" + user_type);
            return false;
        }
        return false;
    }

最后千万不要忘记remove一下ThreadLocal的引用:

 @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        AppContext.clear();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

所以实际场景实际解决,核心是业务,代码简洁只是附带的要求。

到此这篇关于SpringBoot ApplicationListener事件监听接口使用问题探究的文章就介绍到这了,更多相关SpringBoot ApplicationListener内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring注解实现循环重试功能(适用场景分析)

    Spring注解实现循环重试功能(适用场景分析)

    这篇文章主要介绍了Spring注解实现循环重试功能,本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景和注意事项,当需要重试的时候还是很有用的,需要的朋友可以参考下
    2023-04-04
  • 详解Java的内存模型

    详解Java的内存模型

    本文更准确的说法应该是JVM的内存模型,但是这里又牵扯了一些其他的前置知识,主要是想从Java入手,从源头上梳理一遍整个Java底层运行的机制,中间会额外补充一些和题目无关的前置基础,导致主讲内存模型的篇幅所占的比例就不是那么绝对。
    2021-06-06
  • 浅谈Java父子类加载顺序

    浅谈Java父子类加载顺序

    本文主要介绍了Java父子类加载顺序,那么这么长怎么记呀?本文就帮大家总结一下,更有利于记忆,感兴趣的可以了解一下
    2021-08-08
  • JAVA使用commos-fileupload实现文件上传与下载实例解析

    JAVA使用commos-fileupload实现文件上传与下载实例解析

    这篇文章主要介绍了JAVA使用commos-fileupload实现文件上传与下载的相关资料,需要的朋友可以参考下
    2016-02-02
  • MybatisPlusInterceptor依赖变红如何解决,无法识别问题

    MybatisPlusInterceptor依赖变红如何解决,无法识别问题

    这篇文章主要介绍了MybatisPlusInterceptor依赖变红如何解决,无法识别问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • springboot war包部署过程详解

    springboot war包部署过程详解

    这篇文章主要为大家介绍了springboot war包部署过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • 东八区springboot如何配置序列化

    东八区springboot如何配置序列化

    本文主要介绍了东八区springboot如何配置序列化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Spring session redis 修改默认的序列化方法(案例)

    Spring session redis 修改默认的序列化方法(案例)

    这篇文章主要介绍了Spring session redis 修改默认的序列化方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • Java正则表达式学习之分组与替换

    Java正则表达式学习之分组与替换

    这篇文章主要给大家介绍了关于Java正则表达式学习之分组与替换的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Maven配置仓库的方法步骤

    Maven配置仓库的方法步骤

    本文主要介绍了Maven配置仓库的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06

最新评论