對於spring框架來說,最重要的兩大特性就是AOP 和IOC。
以前一直都知道有這兩個東西,在平時做的項目中也常常會涉及到這兩塊,像spring的事務管理什么的,在看了些源碼后,才知道原來事務管理也是用的AOP來實現的。對於IOC的話,平時接觸的就更多了,什么autowired,resource各種注解,就是IOC的各種應用。
一直我也想着能有機會自己動手寫個aop的小DEMO,不過一直沒機會,想到了許多,在網上一搜,基本上都已經有了。今天想到一個用於對service方法進行攔截的功能點,今天決定用springBoot的工程來實現一下。
功能點描述:對某個service的方法執行前,獲取出入參,對入參的參數進行修改,將參數進行替換。然后在這個方法執行完畢后,再對其返回結果進行修改。主要就是對一個方法裝飾一下。說到裝飾,第一想到的是采用裝飾器模式來實現,但裝飾器模式需要對整個代碼的結構進行一些修改,為了達到對以前的代碼不進行任何接觸,且裝飾器模式的局限性較小,所以最好還是用spring的AOP來實現這種對代碼無任何侵入的功能。
service的代碼如下:
@Service public class TestServiceImpl implements TestService { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public ResultVO getResultData(ParamVO paramVO) { return process(paramVO); } private ResultVO process(ParamVO paramVO) { logger.info("----->input INFO:{}", paramVO); ResultVO resultVO = new ResultVO(); resultVO.setCode(200); resultVO.setData(Arrays.asList("123", "456", "789")); resultVO.setMessage("OK!!!!!!!! and your inputParam is" + paramVO.toString()); logger.info("---->return INFO:{}", resultVO.toString()); return resultVO; }
其中入參為paramVO,代碼如下:
public class ParamVO { private String inputParam; private String inputParam2; //getter and setter }
返回的參數ResutVO,代碼如下:
public class ResultVO { private Integer code; private String message; private Object data; //getter and setter }
其調用的入口為一個controller,代碼如下:
@RequestMapping(value = "test") @RestController public class TestController { @Resource private TestService testService; @GetMapping(value = "getResult") public ResultVO getResult(ParamVO paramVO) { ResultVO resultData = testService.getResultData(paramVO); return resultData; }
在正常情況下,按照如上的代碼進行調用將返回如下的信息:

通過返回的信息可以看到,入參是我們在請求參數傳入的inputParam=111和inputParam2=2220
現在要做的就是把入參的參數通過AOP來攔截,並進行修改。對於返回值,也進行一下修改。
首先讓工程引入AOP的包:
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后定義一個Aspect,並指定一個切入點,配置要進行哪些方法的攔截
這里只針對TestSevice這個接口下的getResultData進行攔截
private final String ExpGetResultDataPonit = "execution(* com.haiyang.onlinejava.complier.service.TestService.getResultData(..))"; //定義切入點,攔截servie包其子包下的所有類的所有方法 // @Pointcut("execution(* com.haiyang.onlinejava.complier.service..*.*(..))") //攔截指定的方法,這里指只攔截TestService.getResultData這個方法 @Pointcut(ExpGetResultDataPonit) public void excuteService() { }
對於切入點的配置表達式,可以在網上自行搜索,網上也有許多
在指定了切入點后,就可以對這個切入點excuteService()這個點進行相應的操作了。
可以配置@Before @After 等來進行相應的處理,其代表的意思分別是前置與后置,就是下面代碼這個意思
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result; try { //@Before result = method.invoke(target, args); //@After return result; } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); //@AfterThrowing throw targetException; } finally { //@AfterReturning } }
由於要對入參和最終返回結果進行處理,所以選擇Before和AfterReturning,原來以為after也可以,但看了下,它好像並不能拿到這個方法的返回值,而AfterReturning是一定可以的
攔截后,對應的處理代碼如下:
//執行方法前的攔截方法 @Before("excuteService()") public void doBeforeMethod(JoinPoint joinPoint) { System.out.println("我是前置通知,我將要執行一個方法了"); //獲取目標方法的參數信息 Object[] obj = joinPoint.getArgs(); for (Object argItem : obj) { System.out.println("---->now-->argItem:" + argItem); if (argItem instanceof ParamVO) { ParamVO paramVO = (ParamVO) argItem; paramVO.setInputParam("666666"); } System.out.println("---->after-->argItem:" + argItem); } } /** * 后置返回通知 * 這里需要注意的是: * 如果參數中的第一個參數為JoinPoint,則第二個參數為返回值的信息 * 如果參數中的第一個參數不為JoinPoint,則第一個參數為returning中對應的參數 * returning 限定了只有目標方法返回值與通知方法相應參數類型時才能執行后置返回通知,否則不執行,對於returning對應的通知方法參數為Object類型將匹配任何目標返回值 */ @AfterReturning(value = ExpGetResultDataPonit, returning = "keys") public void doAfterReturningAdvice1(JoinPoint joinPoint, Object keys) { System.out.println("第一個后置返回通知的返回值:" + keys); if (keys instanceof ResultVO) { ResultVO resultVO = (ResultVO) keys; String message = resultVO.getMessage(); resultVO.setMessage("通過AOP把值修改了 " + message); } System.out.println("修改完畢-->返回方法為:" + keys); }
然后再請求一下之前的請求

從這里可以看出,通過AOP的攔截,已經把對應的值修改了,入參inputParam由111改成了666666,返回結果message也加上了幾個字
除了用Before和AfterReturning外,還可以用環繞來實現同樣的功能,如:
/** * 環繞通知: * 環繞通知非常強大,可以決定目標方法是否執行,什么時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值。 * 環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型 */ @Around(ExpGetResultDataPonit) public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) { System.out.println("環繞通知的目標方法名:" + proceedingJoinPoint.getSignature().getName()); processInputArg(proceedingJoinPoint.getArgs()); try {//obj之前可以寫目標方法執行前的邏輯 Object obj = proceedingJoinPoint.proceed();//調用執行目標方法 processOutPutObj(obj); return obj; } catch (Throwable throwable) { throwable.printStackTrace(); } return null; } /** * 處理返回對象 */ private void processOutPutObj(Object obj) { System.out.println("OBJ 原本為:" + obj.toString()); if (obj instanceof ResultVO) { ResultVO resultVO = (ResultVO) obj; resultVO.setMessage("哈哈,我把值修改了" + resultVO.getMessage()); System.out.println(resultVO); } } /** * 處理輸入參數 * * @param args 入參列表 */ private void processInputArg(Object[] args) { for (Object arg : args) { System.out.println("ARG原來為:" + arg); if (arg instanceof ParamVO) { ParamVO paramVO = (ParamVO) arg; paramVO.setInputParam("654321"); } } }
這樣寫,也可以達到相同的目的
切面代碼完整如下:
package com.haiyang.onlinejava.complier.aspect; import com.haiyang.onlinejava.complier.vo.ParamVO; import com.haiyang.onlinejava.complier.vo.ResultVO; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.context.annotation.Configuration; @Configuration @Aspect public class ServiceAspect { private final String ExpGetResultDataPonit = "execution(* com.haiyang.onlinejava.complier.service.TestService.getResultData(..))"; //定義切入點,攔截servie包其子包下的所有類的所有方法 // @Pointcut("execution(* com.haiyang.onlinejava.complier.service..*.*(..))") //攔截指定的方法,這里指只攔截TestService.getResultData這個方法 @Pointcut(ExpGetResultDataPonit) public void excuteService() { } //執行方法前的攔截方法 // @Before("excuteService()") public void doBeforeMethod(JoinPoint joinPoint) { System.out.println("我是前置通知,我將要執行一個方法了"); //獲取目標方法的參數信息 Object[] obj = joinPoint.getArgs(); for (Object argItem : obj) { System.out.println("---->now-->argItem:" + argItem); if (argItem instanceof ParamVO) { ParamVO paramVO = (ParamVO) argItem; paramVO.setInputParam("666666"); } System.out.println("---->after-->argItem:" + argItem); } } /** * 后置返回通知 * 這里需要注意的是: * 如果參數中的第一個參數為JoinPoint,則第二個參數為返回值的信息 * 如果參數中的第一個參數不為JoinPoint,則第一個參數為returning中對應的參數 * returning 限定了只有目標方法返回值與通知方法相應參數類型時才能執行后置返回通知,否則不執行,對於returning對應的通知方法參數為Object類型將匹配任何目標返回值 */ // @AfterReturning(value = ExpGetResultDataPonit, returning = "keys") public void doAfterReturningAdvice1(JoinPoint joinPoint, Object keys) { System.out.println("第一個后置返回通知的返回值:" + keys); if (keys instanceof ResultVO) { ResultVO resultVO = (ResultVO) keys; String message = resultVO.getMessage(); resultVO.setMessage("通過AOP把值修改了 " + message); } System.out.println("修改完畢-->返回方法為:" + keys); } /** * 后置最終通知(目標方法只要執行完了就會執行后置通知方法) */ // @After("excuteService()") public void doAfterAdvice(JoinPoint joinPoint) { System.out.println("后置通知執行了!!!!"); } /** * 環繞通知: * 環繞通知非常強大,可以決定目標方法是否執行,什么時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值。 * 環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型 */ @Around(ExpGetResultDataPonit) public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) { System.out.println("環繞通知的目標方法名:" + proceedingJoinPoint.getSignature().getName()); processInputArg(proceedingJoinPoint.getArgs()); try {//obj之前可以寫目標方法執行前的邏輯 Object obj = proceedingJoinPoint.proceed();//調用執行目標方法 processOutPutObj(obj); return obj; } catch (Throwable throwable) { throwable.printStackTrace(); } return null; } /** * 處理返回對象 */ private void processOutPutObj(Object obj) { System.out.println("OBJ 原本為:" + obj.toString()); if (obj instanceof ResultVO) { ResultVO resultVO = (ResultVO) obj; resultVO.setMessage("哈哈,我把值修改了" + resultVO.getMessage()); System.out.println(resultVO); } } /** * 處理輸入參數 * * @param args 入參列表 */ private void processInputArg(Object[] args) { for (Object arg : args) { System.out.println("ARG原來為:" + arg); if (arg instanceof ParamVO) { ParamVO paramVO = (ParamVO) arg; paramVO.setInputParam("654321"); } } } }
如不進行@Before和@AfterReturing的注釋,最終的結果如下:

控制台打印的日志為:

通過查看打印的結果,我們可以知道@Around @Before @After @AfterReturning這幾個注解的執行順序為:
Around
AroundBefore
before
method.invoke()
AroundAfter
After
AfterReturning
