最近呢xxx接到了一個任務,是須要把AOP打印出的請求日誌,給保存到數據庫。xxx一看這個簡單啊,不就是保存到數據庫嘛。一頓操做猛如虎,過了20分鐘就把這個任務完成了。xxx做爲一個優秀的程序員,發現這樣同步保存會增長了接口的響應時間。這確定難不倒xxx,立即決定使用多線程來處理這個問題。終於在臨近飯點完成了。準備邊吃邊欣賞本身的傑做時,外賣小哥臨時走來了一句,搞這樣麻煩幹啥,你加個@Async
不就能夠了。java
LogAspect程序員
@Slf4j @Aspect @Component public class LogAspect { @Pointcut("execution(* com.hxh.log.controller.*.*(..)))") public void saveLog(){} @Before("saveLog()") public void saveLog(JoinPoint joinPoint) { // 獲取HttpServletRequest ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //獲取請求參數 String[] argNames = signature.getParameterNames(); Object[] args = joinPoint.getArgs(); log.info("請求路徑:{},請求方式:{},請求參數:{},IP:{}",request.getRequestURI(), request.getMethod(), getRequestParam(argNames,args), request.getRemoteAddr()); } /** * 組裝請求參數 * @param argNames 參數名稱 * @param args 參數值 * @return 返回JSON串 */ private String getRequestParam(String[] argNames, Object[] args){ HashMap<String,Object> params = new HashMap<>(argNames.length); if(argNames.length > 0 && args.length > 0){ for (int i = 0; i < argNames.length; i++) { params.put(argNames[i] , args[i]); } } return JSON.toJSONString(params); } }
LoginController數據庫
@RestController public class LoginController { @PostMapping("/login") public String login(@RequestBody LoginForm loginForm){ return loginForm.getUsername() + ":登陸成功"; } }
將項目啓動而後測試一下。 多線程
控制檯已經打印出了請求日誌。app
將日誌保存到數據庫。異步
LogServiceImplide
@Slf4j @Service public class LogServiceImpl implements LogService { @Override public void saveLog(RequestLog requestLog) throws InterruptedException { // 模擬入庫須要的時間 Thread.sleep(2000); log.info("請求日誌保存成功:{}",requestLog); } }
改造一下LogAspect添加日誌入庫測試
@Before("saveLog()") public void saveLog(JoinPoint joinPoint) throws InterruptedException { // 獲取HttpServletRequest ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //獲取請求參數 String[] argNames = signature.getParameterNames(); Object[] args = joinPoint.getArgs(); log.info("請求路徑:{},請求方式:{},請求參數:{},IP:{}",request.getRequestURI(), request.getMethod(), getRequestParam(argNames,args), request.getRemoteAddr()); // 日誌入庫 RequestLog requestLog = new RequestLog(); requestLog.setRequestUrl(request.getRequestURI()); requestLog.setRequestType(request.getMethod()); requestLog.setRequestParam(request.getRequestURI()); requestLog.setIp(request.getRemoteAddr()); logService.saveLog(requestLog); }
測試一下spa
控制檯已經打印出了請求日誌。線程
@Async
因爲保存日誌消耗了2s,致使接口的響應時間也增長了2s。這樣的結果顯然不是我想要的。因此咱們就按外賣小哥的方法,在LogServiceImpl.saveLog()
上加一個@Async
試試。
@Slf4j @Service public class LogServiceImpl implements LogService { @Async @Override public void saveLog(RequestLog requestLog) throws InterruptedException { // 模擬入庫須要的時間 Thread.sleep(2000); log.info("請求日誌保存成功:{}",requestLog); } }
從新啓動項目測試一下。
發現耗時仍是2s多,這外賣小哥在瞎扯吧,因而轉身進入了baidu
的知識海洋遨遊,發現要在啓動類加個@EnableAsync
。
@EnableAsync @SpringBootApplication public class LogApplication { public static void main(String[] args) { SpringApplication.run(LogApplication.class, args); } }
啓動一下項目再來測試一下。
這下可好啓動都失敗了。
不要慌,先看一眼錯誤信息。由於有些service使用了CGLib這種動態代理而不是JDK原生的代理,致使問題的出現。因此咱們須要給@EnableAsync
加上proxyTargetClass=true
。
@Slf4j @EnableAsync(proxyTargetClass=true) @SpringBootApplication public class LogApplication { public static void main(String[] args) { SpringApplication.run(LogApplication.class, args); } }
從新啓動下再測試一下。
這下就成功了嘛,接口響應耗時變成了324ms
,已經不像以前消耗2s
那樣了。
因爲saveLog()
是沒有返回值,假如碰到有返回值的狀況該咋辦呢?使用Future<T>
便可。
@Slf4j @Service public class LogServiceImpl implements LogService { @Async @Override public Future<Boolean> saveLog(RequestLog requestLog) throws InterruptedException { // 模擬入庫須要的時間 Thread.sleep(2000); log.info("請求日誌保存成功:{}",requestLog); return new AsyncResult<>(true); } }
既然是異步方法,確定是用其餘的線程執行的,固然能夠配置相應的線程池了。
@Configuration public class ThreadConfig { /** * 日誌異步保存輸出線程池 * @return 返回線程池 */ @Bean("logExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("logExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); return executor; } }
在使用@Async
的時候指定對應的線程池就行了。
@Slf4j @Service public class LogServiceImpl implements LogService { @Override @Async("logExecutor") public Future<Boolean> saveLog(RequestLog requestLog) throws InterruptedException { // 模擬入庫須要的時間 Thread.sleep(2000); log.info("請求日誌保存成功:{}",requestLog); return new AsyncResult<>(true); } }
@EnableAsync
。 @Async
標註的方法,稱之爲異步方法;這些方法將在執行的時候,將會在獨立的線程中被執行,調用者無需等待它的完成,便可繼續其餘的操做。
雖然本身維護線程池也是能夠實現相應的功能,可是我仍是推薦使用SpringBoot
自帶的異步方法,簡單方便,只須要@Async
和@EnableAsync
就能夠了。
爲何外賣小哥能看懂我寫的代碼?難道我之後也要去xxx?
若是以爲對你有幫助,能夠多多評論,多多點贊哦,也能夠到個人主頁看看,說不定有你喜歡的文章,也能夠隨手點個關注哦,謝謝。