AOP:面向切面編程
簡介
AOP解決的問題:將核心業務代碼與外圍業務(日志記錄、權限校驗、異常處理、事務控制)代碼分離出來,提高模塊化,降低代碼耦合度,使職責更單一。
AOP應用場景:
日志記錄、權限校驗、異常處理、事務控制等
相關概念
圖片來源:
https://raw.githubusercontent.com/WarframePrimer/mysmart4j/master/img/AOP.jpg
joinPoint:連接點。在spring中只支持方法連接點,連接點指的是可以使用advice(增強)的地方,例如一個類中有5個方法,那么這5個方法,那么這5個方法都可以是連接點。
pointcut:切點。可理解為實實在在的連接點,即切入advice(增強)的點。例如
一個類中有5個方法,其中有3個方法(連接點)需要織入advice(增強),那么這3個需要織入advice的連接點就是切點。
advice:增強。實際中想要添加的功能,如日志、權限校驗。
before:前置增強,目標方法執行前之前執行。
after:后置增強,目標方法執行后執行。
around:環繞增強,在目標方法執行時執行,可控制目標方法是否執行。
after throwing:異常增強,目標方法拋出異常時執行。
weaving:織入。即對方法的增強,將切面的代碼織入(應用)到目標函數的過程。
introduction advice:引入增強。即對類的增強。
advisor:切面。由切點和增強相結合而成,定義增強應用到哪些切點上。
Aop的兩種實現
pom.xml依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lnjecit</groupId> <artifactId>chapter4</artifactId> <version>1.0-SNAPSHOT</version> <properties> <spring.version>4.1.7.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> </dependencies> </project>
Spring aop
基於代理(jdk動態代理、cglib動態代理)實現的aop
Spring aop使用了兩種代理機制。一種是jdk動態代理,另一種是cglib動態代理。
Jdk動態代理只支持接口代理,cglib支持類的代理。
編程式
前置增強 before advice
前置增強:實現MethodBeforeAdvice接口,執行目標方法前執行before方法
/** * 編程式前置增強 */ public class UserBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before"); } }
后置增強 after advice
后置增強:實現AfterReturningAdvice接口,執行目標方法后執行afterReturning方法
/** * 編程式后置增強 */ public class UserAfterAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("After"); } }
環繞增強 around advice
環繞增強:實現MethodInterceptor接口,執行目標方法前后執行invoke方法
/** * 編程式環繞增強 **/ public class UserAroundAdvice implements MethodInterceptor{ @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before"); Object result = invocation.proceed(); System.out.println("After"); return result; } }
用戶服務接口類:
public interface UserService { void queryAll(); }
用戶服務接口實現類:
public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); } }
測試代碼:
public class Test { public static void main(String[] args) { /** * 測試前置增強和后置增強 */ ProxyFactory proxyFactory = new ProxyFactory();//創建代理工廠 proxyFactory.setTarget(new UserServiceImpl());//射入目標類對象 proxyFactory.addAdvice(new UserBeforeAdvice());//添加前置增強 proxyFactory.addAdvice(new UserAfterAdvice());//添加后置增強 UserService userService = (UserService) proxyFactory.getProxy();//從代理工廠獲取代理 userService.queryAll();//調用代理的方法 /** * 測試環繞增強 */ ProxyFactory proxyFactory2 = new ProxyFactory();//創建代理工廠 proxyFactory2.setTarget(new UserServiceImpl());//射入目標類對象 proxyFactory2.addAdvice(new UserAroundAdvice());//添加環繞增強 UserService userService2 = (UserService) proxyFactory2.getProxy();//從代理工廠獲取代理 userService2.queryAll();//調用代理的方法 } }
聲明式(基於xml配置)
環繞增強 around advice
spring-aop.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <!--配置代理--> <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--需要代理的接口--> <property name="interfaces" value="com.lnjecit.chapter4.user.UserService"/> <!--接口實現類--> <property name="target" ref="userServiceImpl"/> <!--攔截器名稱(即增強類名稱)--> <property name="interceptorNames"> <list> <value>userAroundAdvice</value> </list> </property> </bean> </beans>
用戶服務接口類:
public interface UserService { void queryAll(); }
用戶服務接口實現類:
@Component public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); } }
測試環繞增強代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml"); // 從context中獲取id為userServiceProxy的代理對象 UserService userServiceProxy = (UserService) applicationContext.getBean("userServiceProxy"); // 調用代理的方法 userServiceProxy.queryAll(); } }
拋出增強 throws advice
拋出增強:實現ThrowsAdvice接口。當執行目標方法出現異常會執行拋出增強中的afterThrowing方法。
/** * 拋出增強 */ @Component public class UserThrowAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target, Exception e) { System.out.println("Throw exception:"); System.out.println("Target class name:" + target.getClass().getName()); System.out.println("Method name: " + method.getName()); System.out.println("Exception message:" + e.getMessage()); } }
將UserServiceImpl類修改如下:故意拋出運行時異常,用於測試拋出增強
@Component public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); throw new RuntimeException("Error"); } }
Spring-aop.xml配置文件修改如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <!--配置代理--> <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--需要代理的接口--> <property name="interfaces" value="com.lnjecit.chapter4.user.UserService"/> <!--接口實現類--> <property name="target" ref="userServiceImpl"/> <!--攔截器名稱(即增強類名稱)--> <property name="interceptorNames"> <list> <value>userThrowAdvice</value> </list> </property> </bean> </beans>
測試代碼與環繞增強的測試代碼相同,不再復制。
切面 advisor
advisor(切面)封裝了advice(增強)和pointcut(切點)
在UserService接口中添加兩個方法query、save.
UserService代碼如下:
public interface UserService { void queryAll(); void query(); void save(); }
UserServiceImpl代碼如下:
@Component public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); } @Override public void query() { System.out.println("根據條件查詢用戶"); } @Override public void save() { System.out.println("新增用戶"); } }
spring-aop.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <!--配置一個切面--> <bean id="userServiceAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!--增強--> <property name="advice" ref="userAroundAdvice"/> <!--切點(正則表達式:匹配UserServiceImpl類中以query開頭的方法)--> <property name="pattern" value="com.lnjecit.chapter4.user.UserServiceImpl.query.*"/> </bean> <!--配置代理--> <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--目標類--> <property name="target" ref="userServiceImpl"/> <!--切面--> <property name="interceptorNames" value="userServiceAdvisor"/> <!--代理目標類--> <property name="proxyTargetClass" value="true"/> </bean> </beans>
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml"); // 從context中獲取id為userServiceProxy的代理對象 UserService userServiceProxy = (UserService) applicationContext.getBean("userServiceProxy"); // 調用代理的方法 userServiceProxy.queryAll(); userServiceProxy.query(); userServiceProxy.save(); } }
測試結果如下:
Before
查詢全部用戶並返回
After
Before
根據條件查詢用戶
After
新增用戶
可以看出UserService中的queryAll、query方法被攔截,執行這兩個方法前后執行了環繞增強代碼。而save方法沒有被攔截。
自動代理
aspectj
基於注解
環繞增強 around advice
使用@Aspect注解定義切面類UserAdvisor
/** * 切面 */ @Aspect @Component public class UserAdvisor { @Around("execution(* com.lnjecit.chapter4.user.UserServiceImpl.*(..))") public Object arount(ProceedingJoinPoint joinPoint) throws Throwable { before(); Object result = joinPoint.proceed(); after(); return result; } public void before() { System.out.println("Before"); } public void after() { System.out.println("After"); } }
切點表達式:
execution(* com.lnjecit.chapter4.user.UserServiceImpl.*(..))
l Execution表示要攔截的方法
l 第一個“*”表示方法返回值任意
l 第二個“*”表示匹配類中的所有方法
l (..)表示方法參數任意
spring-aspectj.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <!--默認為false,使用JDK動態代理 設置為true,啟用cglib動態代理--> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj.xml"); // 從context中獲取id為userServiceImpl的對象 UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl"); userServiceImpl.queryAll(); userServiceImpl.query(); userServiceImpl.save(); } }
攔截指定注解
簡單案例:使用@Annotation注解攔截Log注解,記錄日志
定義Log注解
/** * 日志注解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface Log { }
定義切面LogAspect
@Aspect
@Component
public class LogAspect {
@Before(value = "@annotation(com.lnjecit.chapter4.user.Log)") public void before(JoinPoint joinPoint) { // 目標類名稱 String targetClazzName = joinPoint.getTarget().getClass().getName(); // 目標方法名稱 String targetMethodName = joinPoint.getSignature().getName(); System.out.println("執行目標方法" + targetClazzName + "." + targetMethodName +"前,記錄日志"); // 可在此處將日志異步存儲到數據庫表中 } }
在UserServiceImpl類的save方法上加上Log注解
@Component public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); // throw new RuntimeException("Error"); } @Override public void query() { System.out.println("根據條件查詢用戶"); } @Log @Override public void save() { System.out.println("新增用戶"); } }
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj.xml"); // 從context中獲取id為userServiceImpl的對象 UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl"); userServiceImpl.save(); } }
測試結果如下:
執行目標方法com.lnjecit.chapter4.user.UserServiceImpl.save前,記錄日志
新增用戶
根據結果可以看出執行save方法之前,執行了LogAspect中的前置增強。
基於配置
除了使用Aspect注解外,還可以使用xml配置的方式來實現aspect的應用
環繞增強 around advice
切面類UserAdvisor
/** * 切面 */ public class UserAdvisor { public Object arount(ProceedingJoinPoint joinPoint) throws Throwable { before(); Object result = joinPoint.proceed(); after(); return result; } public void before() { System.out.println("Before"); } public void after() { System.out.println("After"); } }
spring-aspectj.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <bean id="userServiceImpl" class="com.lnjecit.chapter4.user.UserServiceImpl"/> <bean id="userAdvisor" class="com.lnjecit.chapter4.user.UserAdvisor"/> <aop:config> <!--切面--> <aop:aspect ref="userAdvisor"> <!--環繞增強--> <aop:around method="arount" pointcut="execution(* com.lnjecit.chapter4.user.UserServiceImpl.*(..))"></aop:around> </aop:aspect> </aop:config> </beans>
UserServiceImpl:
public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); } @Override public void query() { System.out.println("根據條件查詢用戶"); } @Log @Override public void save() { System.out.println("新增用戶"); } }
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj.xml"); // 從context中獲取id為userServiceImpl的對象 UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl"); userServiceImpl.queryAll(); userServiceImpl.query(); userServiceImpl.save(); } }
測試結果與使用注解實現效果相同。
攔截指定注解
Log注解:
/** * 日志注解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface Log { }
切面LogAspect:
/** * 日志切面 */ public class LogAspect { public void before(JoinPoint joinPoint) {
// 目標類名稱
String targetClazzName = joinPoint.getTarget().getClass().getName();
// 目標方法名稱
String targetMethodName = joinPoint.getSignature().getName();
System.out.println("執行目標方法" + targetClazzName + "." + targetMethodName +"前,記錄日志");
// 可在此處將日志異步存儲到數據庫表中
}
spring-aspectj.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="userServiceImpl" class="com.lnjecit.chapter4.user.UserServiceImpl"/> <bean id="logAspect" class="com.lnjecit.chapter4.user.LogAspect"/> <aop:config> <!--切面--> <aop:aspect ref="logAspect"> <!--前置增強--> <aop:before method="before" pointcut="@annotation(com.lnjecit.chapter4.user.Log)"></aop:before> </aop:aspect> </aop:config> </beans>
UserService、UserServiceImpl類與環繞通知中代碼中相同。
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj.xml"); // 從context中獲取id為userServiceImpl的對象 UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl"); userServiceImpl.queryAll(); userServiceImpl.query(); userServiceImpl.save(); } }
測試結果與基於注解測試結果相同。
Spring aop與aspectJ的區別
1、織入的時期不同
spring aop采用的動態織入,而aspectJ是靜態織入。
靜態織入:指在編譯時期就織入,即:編譯出來的class文件,字節碼就已經被織入了。
動態織入:分靜動兩種,靜則指織入過程只在第一次調用時執行;動則指根據代碼動態運行的中間狀態來決定如何操作,每次調用target的時候都執行。
Spring aspectJ簡單應用案例
日志記錄
可參考前面案例中的定義日志記錄,使用切面記錄日志。
性能監控
定義性能監控切面:
/** * 性能監控切面 */ @Aspect @Component public class MonitorAspect { @Around("execution(* com.lnjecit.chapter4.user.UserServiceImpl.*(..))") public Object arount(ProceedingJoinPoint joinPoint) throws Throwable { // 目標類名稱 String targetClazzName = joinPoint.getTarget().getClass().getName(); // 目標類方法名稱 String targetMethodName = joinPoint.getSignature().getName(); // 計時並調用目標函數 long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long time = System.currentTimeMillis() - start; System.out.println("執行" + targetClazzName + "." + targetMethodName + "方法耗時" + time + "毫秒"); // 可在此處將監控信息存儲 return result; } }
UserService:
public interface UserService { void queryAll(); void query(); void save(); }
UserServiceImpl:
@Component public class UserServiceImpl implements UserService { @Override public void queryAll() { System.out.println("查詢全部用戶並返回"); } @Override public void query() { System.out.println("根據條件查詢用戶"); } @Override public void save() { System.out.println("新增用戶"); }
spring-aspectj.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--掃描指定包--> <context:component-scan base-package="com.lnjecit.chapter4.user"/> <!--默認為false,使用JDK動態代理 設置為true,啟用cglib動態代理--> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
測試代碼:
public class Test { public static void main(String[] args) { // 獲取spring context ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj.xml"); // 從context中獲取id為userServiceImpl的對象 UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl"); userServiceImpl.queryAll(); userServiceImpl.query(); userServiceImpl.save(); } }
測試結果如下:
查詢全部用戶並返回
執行com.lnjecit.chapter4.user.UserServiceImpl.queryAll方法耗時54毫秒
根據條件查詢用戶
執行com.lnjecit.chapter4.user.UserServiceImpl.query方法耗時0毫秒
新增用戶
執行com.lnjecit.chapter4.user.UserServiceImpl.save方法耗時1毫秒
參考資料:
1、 https://blog.csdn.net/javazejian/article/details/56267036
2、 《架構探險 從零開始寫javaweb框架》