在《Spring AOP初級——入門及簡單應用》中對AOP作了簡要的介紹,以及一些專業術語的解釋,同時寫了一個簡單的Spring AOPdemo。本文將繼續探討Spring AOP在實際場景中的應用。
對用戶操作日志的記錄是很常見的一個應用場景,本文選取“用戶管理”作為本文Spring AOP的示例。當然,該示例只是對真實場景的模擬,實際的環境一定比該示例更復雜。
該示例的完整代碼路徑在https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/Spring%20AOP%E4%B8%AD%E7%BA%A7%E2%80%94%E2%80%94%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF。本文僅對Spring AOP相關的代碼進行講解。
在這個示例中首次采用RESTful架構風格,對於以下RESTful API的設計可能並不完美,如果有熟悉、精通RESTful架構風格的朋友希望能夠指出我的錯誤。
使用RESTful的前后端分離架構風格后,我感受到了前所未有的暢快,所以此次示例並沒有前端頁面的展示,完全使用JUnit進行單元測試包括對HTTP請求的Mock模擬,這部分代碼不會進行詳細講解,之后會繼續深入JUnit單元測試的一些學習研究。
數據庫只有一張表:
回到正題,我們回顧下切面由哪兩個部分組成: 通知 切點 首先明確我們需要在何時記錄日志:
通知
切點
首先明確我們需要在何時記錄日志:
1. 查詢所有用戶時,並沒有參數(此示例沒有作分頁),只有在返回時才會有數據的返回,所以對查詢所有用戶的方法采用返回通知(AfterReturning)。
2. 新增用戶時,會帶有新增的參數,此時可采用前置通知(Before)。
3. 修改用戶時,也會帶有新增的參數,此時同樣采用前置通知(Before)。
4. 刪除用戶時,通常會帶有唯一標識符ID,此時采用前置通知(Before)記錄待刪除的用戶ID。
在明確了通知類型后,此時我們需要明確切點,也就是在哪個地方記錄日志。當然上面實際已經明確了日志記錄的位置,但主要是切面表達式的書寫。 在有了《Spring AOP初級——入門及簡單應用》的基礎,相信對日志切面類已經比較熟悉了:
1 package com.manager.aspect; 2 3 import org.apache.log4j.Logger; 4 import org.aspectj.lang.JoinPoint; 5 import org.aspectj.lang.annotation.*; 6 import org.springframework.stereotype.Component; 7 8 import java.util.Arrays; 9 10 /** 11 * 日志切面 12 * Created by Kevin on 2017/10/29. 13 */ 14 @Aspect 15 @Component 16 public class LogAspect { 17 /** 18 * 操作日志文件名 19 */ 20 private static final String OPERATION_LOG_NAME = "operationLog"; 21 private static final String LOG_FORMATTER = "%s.%s - %s"; 22 Logger log = Logger.getLogger(OPERATION_LOG_NAME); 23 /** 24 * 對查詢方法記錄日志的切點 25 */ 26 @Pointcut("execution(* com.manager..*.*Controller.query*(..))") 27 public void query(){} 28 29 /** 30 * 對新增方法記錄日志的切點 31 */ 32 @Pointcut("execution(* com.manager..*.*Controller.add*(..))") 33 public void add(){} 34 35 /** 36 * 對修改方法記錄日志的切點 37 */ 38 @Pointcut("execution(* com.manager..*.*Controller.update*(..))") 39 public void update(){} 40 41 /** 42 * 對刪除方法記錄日志的切點 43 */ 44 @Pointcut("execution(* com.manager..*.*Controller.delete*(..))") 45 public void delete(){} 46 47 @AfterReturning(value = "query()", returning = "rvt") 48 public void queryLog(JoinPoint joinPoint, Object rvt) { 49 String className = joinPoint.getTarget().getClass().getName(); 50 String methodName = joinPoint.getSignature().getName(); 51 String returnResult = rvt.toString(); 52 log.info(String.format(LOG_FORMATTER, className, methodName, returnResult)); 53 } 54 55 @Before("add()") 56 public void addLog(JoinPoint joinPoint) { 57 String className = joinPoint.getTarget().getClass().getName(); 58 String methodName = joinPoint.getSignature().getName(); 59 Object[] params = joinPoint.getArgs(); 60 log.info(String.format(LOG_FORMATTER, className, methodName, Arrays.toString(params))); 61 } 62 63 @Before("update()") 64 public void updateLog(JoinPoint joinPoint) { 65 String className = joinPoint.getTarget().getClass().getName(); 66 String methodName = joinPoint.getSignature().getName(); 67 Object[] params = joinPoint.getArgs(); 68 log.info(String.format(LOG_FORMATTER, className, methodName, Arrays.toString(params))); 69 } 70 71 @Before("delete()") 72 public void deleteLog(JoinPoint joinPoint) { 73 String className = joinPoint.getTarget().getClass().getName(); 74 String methodName = joinPoint.getSignature().getName(); 75 Object[] params = joinPoint.getArgs(); 76 log.info(String.format(LOG_FORMATTER, className, methodName, Arrays.toString(params))); 77 } 78 }
上面的日志切面類中出現了JointPoint類作為參數的情況,這個參數能夠傳遞被通知方法的相信,例如被通知方法所處的類以及方法名等。在第47行中的Object rvt參數就是獲取被通知方法的返回值。 上面的切面並沒有關注被通知方法的參數,如果要使得切面和被通知方法參數參數關聯可以使用以下的方式:
@Pointcut("execution(* com.xxx.demo.Demo.method(int)) && args(arg)") public void aspectMethod(int arg){} @Before(“aspectMedhot(arg)”) public void method(int arg) { //此時arg參數就是被通知方法的參數 }
本例中最主要的切面部分就完成了。注意在結合Spring時需要在applicationContext.xml中加入以下語句:
<!--啟用AspectJ自動代理,其中proxy-target-class為true表示使用CGLib的代理方式,false表示JDK的代理方式,默認false--> <aop:aspectj-autoproxy />
示例中關於log4j、pom.xml依賴、JUnit如何結合Spring進行單元測試等等均可可以參考完整代碼。特別是JUnit是很值得學習研究的一部分,這部分在將來慢慢我也會不斷學習推出新的博客,在這里就只貼出JUnit的代碼,感興趣的可以瀏覽一下:
1 package com.manager.user.controller; 2 3 import com.fasterxml.jackson.databind.ObjectMapper; 4 import com.manager.user.pojo.User; 5 import org.junit.Before; 6 import org.junit.Test; 7 import org.junit.runner.RunWith; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.http.MediaType; 10 import org.springframework.test.context.ContextConfiguration; 11 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 import org.springframework.test.context.web.WebAppConfiguration; 13 import org.springframework.test.web.servlet.MockMvc; 14 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 15 import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 16 import org.springframework.test.web.servlet.setup.MockMvcBuilders; 17 import org.springframework.web.context.WebApplicationContext; 18 19 import static org.junit.Assert.assertNotNull; 20 21 /** 22 * UserController單元測試 23 * Created by Kevin on 2017/10/26. 24 */ 25 @RunWith(SpringJUnit4ClassRunner.class) 26 @ContextConfiguration({"classpath*:applicationContext.xml", "classpath*:spring-servlet.xml"}) 27 @WebAppConfiguration 28 public class UserControllerTest { 29 @Autowired 30 private WebApplicationContext wac; 31 private MockMvc mvc; 32 33 @Before 34 public void initMockHttp() { 35 this.mvc = MockMvcBuilders.webAppContextSetup(wac).build(); 36 } 37 38 @Test 39 public void testQueryUsers() throws Exception { 40 mvc.perform(MockMvcRequestBuilders.get("/users")) 41 .andExpect(MockMvcResultMatchers.status().isOk()); 42 } 43 44 @Test 45 public void testAddUser() throws Exception { 46 User user = new User(); 47 user.setName("kevin"); 48 user.setAge(23); 49 mvc.perform(MockMvcRequestBuilders.post("/users") 50 .contentType(MediaType.APPLICATION_JSON_UTF8) 51 .content(new ObjectMapper().writeValueAsString(user))) 52 .andExpect(MockMvcResultMatchers.status().isOk()) 53 .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("kevin")) 54 .andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23)); 55 } 56 57 @Test 58 public void testQueryUserById() throws Exception { 59 User user = new User(); 60 user.setId(8); 61 mvc.perform(MockMvcRequestBuilders.get("/users/" + user.getId())) 62 .andExpect(MockMvcResultMatchers.status().isOk()) 63 .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("kevin")) 64 .andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23)); 65 66 } 67 68 @Test 69 public void testUpdateUserById() throws Exception { 70 User user = new User(); 71 user.setId(9); 72 user.setName("tony"); 73 user.setAge(99); 74 mvc.perform(MockMvcRequestBuilders.put("/users/" + user.getId()) 75 .contentType(MediaType.APPLICATION_JSON_UTF8) 76 .content(new ObjectMapper().writeValueAsString(user))) 77 .andExpect(MockMvcResultMatchers.status().isOk()); 78 } 79 80 @Test 81 public void testDeleteUserById() throws Exception { 82 long id = 10; 83 mvc.perform(MockMvcRequestBuilders.delete("/users/" + id)) 84 .andExpect(MockMvcResultMatchers.status().isOk()); 85 } 86 }
有了初級和中級,接下來必然就是Spring AOP高級——源碼實現。
這是一個能給程序員加buff的公眾號