SpringBoot ApplicationListener事件监听接口使用问题探究
作者:ForestSpringH 发布时间:2023-03-04 22:44:13
终日惶惶,不知归路;一日写起代码,突发奇想,若是在运行时发现自定义上下文的数据丢失,我们该如何解决处理数据丢失的问题?
问题复现一下,大家看下面的代码,观察是否有问题,又该如何解决这个问题:
@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);
}
所以实际场景实际解决,核心是业务,代码简洁只是附带的要求。
来源:https://blog.csdn.net/m0_59588838/article/details/129891333


猜你喜欢
- ##创建测试类 新建Java工程创建测试类如下代码:(创建文件验证定时器是否执行)package makeFile;import java.
- 说到网络,相信大家都对TCP、UDP和HTTP协议这些都不是很陌生,学习这部分应该先对端口、Ip地址这些基础知识有一定了解,后面我们都是直接
- 语法糖指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程
- 看完我上一篇文章,「你都理解创建线程池的参数吗?」之后,当遇到这种问题,你觉得你完全能够唬住面试官了,50k轻松到手。殊不知,要是面试官此刻
- RabbitMQ主要有六种种工作模式,本文整合SpringBoot分别介绍工作模式的实现。前提概念生产者消息生产者或者发送者,使用P表示:队
- 在默认情况下,对象的Equals(object o)方法(基类Object提供),是比较两个对象变量是否引用同一对象。我们要必须我自己的对象
- 客户端代码:/// <summary>/// 批量上传图片/// </summary>/// <param n
- 0.Springboot项目创建通过https://start.spring.io/生成纯净的一个springboot工程1.引入Activ
- ServletContext 基础知识获取 ServletContext对象有两种方式可以获取:使用 servletconfig 对象获取使
- 简介单例指的是只能存在一个实例的类(在C#中,更准确的说法是在每个AppDomain之中只能存在一个实例的类,它是软件工程中使用最多的几种模
- 所以对于应用层用着还不是很方便。最近做一个项目顺便就封装了一个调用默认打印机的类。虽说有几个小bug,但对于目前来说,已经满足需求了。以后不
- 1、前言随着技术的发展,微信的一系列服务渗透进了我们的生活,但是我们应该怎样进行微信方面的开发呢。相信很多的小伙伴们都很渴望知道吧。这篇文章
- 记录使用Scroller实现平滑滚动,效果图如下:一、自定义View中实现View的平滑滚动public class ScrollerVie
- 引言最近,各大平台都新增了评论区显示发言者ip归属地的功能,例如哔哩哔哩,微博,知乎等等。Java 中是如何获取 IP&
- Android中的Service是用于后台服务的,当应用程序被挂到后台的时候,问了保证应用某些组件仍然可以工作而引入了Service这个概念
- 需求描述 今日需求是删除资源时同时删除与该资源绑定的角色数据,有两张表,资源表、
- 这篇文章主要讲述服务追踪组件zipkin,Spring Cloud Sleuth集成了zipkin组件。一、简介Add sleuth to
- 使用 replace 函数动态填充字符串String str="Hello {0},我是 {1},今年{2}岁"
- 1 原码、反码、补码原码:将十进制转化为二进制即原码;反码:正数的反码与原码相同,负数的反码(除却最高位的符号位不变)与原码相反
- 有时候编译器、处理器的优化会导致runtime与我们设想的不一样,为此Java对编译器和处理器做了一些限制,JAVA内存模型(JMM)将这些