問題複現一下,大家看下面的程式碼,觀察是否有問題,又該如何解決這個問題:
@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
解決想法(實際上不是這麼解決的,但是也可以這麼做,代價是效能耗費高):
設計一個監聽者,一個發布者,在請求進入的方法上進行切面處理,切面檢查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事件監聽介面使用問題怎麼解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!