1. 程式人生 > 實用技巧 >spring-aop筆記

spring-aop筆記

2、Spring-AOP

2.1、代理【Proxy】

  • 動態代理:

    • 特點:位元組碼誰用誰建立,誰用誰載入

    • 作用:不修改原始碼的基礎上對方法增強

    • 分類:

      • 基於介面的動態代理

      • 基於子類的動態代理

    • 基於介面的動態代理

      • 涉及的類:Proxy

      • 提供者:JDK官方

    • 如何建立代理物件:

      • 使用Proxy類中的newProxyInstance方法

    • 建立代理物件的要求:

      • 被代理類最少實現一個介面,如果沒有則不能使用

    • newProxyInstance方法的引數:

      • ClassLoader:類載入器

        • 它是用於載入代理物件位元組碼的.和被代理物件使用相同的類載入器。固定寫法。

      • Class[]

        • 它是用於讓代理物件和被代理物件有相同方法。固定寫法。

      • InvocationHandler

        • 它是讓我們寫如何代理。我們一般都是寫一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。

        • 此介面的實現類都是誰用誰寫

來測試一下:

2.1.1、未使用代理

1.定義一個介面,要被代理的介面:Producer【提供者】

//生產者
public interface Producer {
//銷售
public void saleProduct(float money);
//售後
public void afterService(float money);
}

2.定義一個類,ProducerImpl【提供者】(要被代理的角色)

//生產者
public class ProducerImpl implements Producer {
//銷售
public void saleProduct(float money){
System.out.println("銷售產品,拿到錢:" + money);
}
//售後
public void afterService(float money){
System.out.println("售後服務,拿到錢:" + money);
}
}

3.定義一個類,Client【消費者】(未使用代理,直接面向廠家)

//模擬一個消費者(未使用代理)
public class Client {
public static void main(String[] args){
IProducer producer = new Producer();
producer.saleProduct(1000f);

}
}

結果:

2.1.2、使用了代理

1.定義一個類,ProxyMan【代理人】(不收中介費的時候)

public class ProxyMan implements InvocationHandler {

private Object target;

public void setTarget(Object target) {
this.target = target;
}

public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader() ,target.getClass().getInterfaces() ,this);
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target ,args);
}
}

2.定義一個類,Client【消費者】(使用了代理)

//模擬一個消費者
public class Client {

public static void main(String[] args){
IProducer producer = new Producer();
// producer.saleProduct(1000f);
//代理模式
ProxyMan pm = new ProxyMan();
pm.setTarget(producer);
IProducer producerPoxy = (IProducer)pm.getProxy();
producerPoxy.afterService(1000f);

}
}

結果:

3.當代理人收代理費時(抽取兩成):

public class ProxyMan implements InvocationHandler {

private Object target;

public void setTarget(Object target) {
this.target = target;
}

public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader() ,target.getClass().getInterfaces() ,this);
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value = null;
Float money = (Float) args[0];
if("saleProduct".equals(method.getName())) {
value = method.invoke(target, money * 0.8f);
}
if("afterService".equals(method.getName())){
value = method.invoke(target ,args);
}
return value;
}
}

結果:

被成功抽取兩成!

在不改變原有程式碼的時候,改變傳輸的資訊。不僅僅可以改變值,還可以增加其他東西,代理模式大多數情況用在增加日誌等功能。

2.2、Spring中的AOP

2.2.1、AOP相關術語:

  • Joinpoint(連線點):

    • 所謂連線點是指那些被攔截到的點。在spring中,這些點指的是方法,因為spring只支援方法型別的連線點。

  • Pointout(切入點):

    • 所謂切入點是指我們要對那些Joinpoint進行攔截的定義。

  • Advice(通知/增強)

    • 所謂通知是指攔截到Joinpoint之後所要做的事情就是通知。

    • 通知型別:前置通知,後置通知,異常通知,環繞通知。

  • Introduction(引介):

    • 引介是一種特殊的通知,在不修改類程式碼的前提下,Introduction可以在執行期為類動態的新增一些方法或Field。

  • Target(目標物件)

    • 代理的目標物件(也就是說被代理的物件)

  • Weaving(織入):

    • 是指把增強應用到目標物件來建立新的代理物件的過程。

    • spring採用動態代理織入,而AspectJ採用編譯期織入和類裝載期織入。

  • Proxy(代理):

    • 一個類被AOP織入增強後,就產生一個結果代理類。

  • Aspect(切面):

    • 是切入點和通知(引介)的結合。

2.2.2、AOP的配置內容

spring中基於XML的AOP配置步驟:

  1. 把通知Bean也較給spring來管理

    <!-- 配置Logger類 -->
    <bean id="logger" class="com.gzk.utils.Logger"></bean>

  2. 使用aop:config標籤表明開始AOP的配置

  3. 使用aop:aspect標籤表明配置切面

    • id屬性:是給切面提供一個唯一標識

    • ref屬性:是指定通知類bean的ID

  4. 在aop:aspect標籤的內部使用對應標籤來配置通知的型別

    • 我們現在示例是讓pringLog方法在切入點方法執行之前:所以是前置通知

    • aop:before:表示配置前置通知

      • method屬性:用於指定Logger類中哪個方法是前置通知

      • pointcut屬性:用於指定切入點表示式,該表示式的含義指的是對業務層中哪些方法增強

    • aop:after-returning:表示配置後置通知

      • method屬性:用於指定Logger類中哪個方法是後置通知

      • pointcut屬性:用於指定切入點表示式,該表示式的含義指的是對業務層中哪些方法增強

    • aop:after-throwing:表示配置異常通知

      • method屬性:用於指定Logger類中哪個方法是異常通知

      • pointcut屬性:用於指定切入點表示式,該表示式的含義指的是對業務層中哪些方法增強

    • aop:after:表示配置最終通知

      • method屬性:用於指定Logger類中哪個方法是最終通知

      • pointcut屬性:用於指定切入點表示式,該表示式的含義指的是對業務層中哪些方法增強

    • 切入點表示式的寫法:

      • 關鍵字:execution(表示式)

      • 表示式:

        • 訪問修飾符 返回值 包名.包名...類名.方法名(引數列表)

      • 標準表示式寫法(例):

        • public void com.gzk.servicce.UserServiceImpl.test()

      • 訪問修飾符可以省略

        • void com.gzk.servicce.UserServiceImpl.test()

      • 返回值可以使用萬用字元,表示任意返回值

        • * com.gzk.servicce.UserServiceImpl.test()

      • 包名可以使用萬用字元,表示任意包。但是有幾級包,就需要寫幾個*

        • * *.*.*.UserServiceImpl.test()

      • 包名可以使用..表示當前包及其子包

        • * *..UserServiceImpl.test()

      • 類名和方法名都可以使用*來實現通配

        • * *..*.*()

      • 引數列表:

        • 可以直接寫資料型別:

          • 基本型別直接寫名稱:int(例)

            • * *..*.*(int)

          • 引用型別寫包名.類名的方式:java.lang.String(例)

            • * *..*.*(java.lang.String)

        • 可以使用萬用字元*表示任意型別,但是必須有引數

          • * *..*.*(*)

        • 可以使用..表示有無引數均可,有引數可以使任意型別

          • * *..*.*(..)

      • 全通配寫法:

        • * *..*.*(..)

      • 實際開發中,很少用全通配寫法,因為會全部被代理,切入點表示式的通常寫法:

        • 切到業務層或者要被代理的層(或包)實現類下的所有方法:

          • * com.gzk.service.*.*(..)

2.2.3、測試(非註解)

測試四種常用通知型別【前置通知,後置通知,異常通知,最終通知】

1.隨意配置mapper和service包下的類,用作測試

public interface UserMapper {
void test();
}
public class UserMapperImpl implements UserMapper {
public void test() {
System.out.println("測試中。。。");
}
}
public interface UserService {
void test();
}
public class UserServiceImpl implements UserService {

private UserMapper userMapper;

public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}

public void test() {
int i = 1/0;
userMapper.test();
}
}

2.將UserServiceImpl設為被代理的類

3.新建一個utils包,建立一個Logger類【代理類】

public class Logger {

public void beforeLogger(){
System.out.println("前置通知");
}

public void afterReturningLogger(){
System.out.println("後置通知");
}

public void afterThrowingLogger(){
System.out.println("異常通知");
}

public void afterLogger(){
System.out.println("最終通知");
}

}

4.配置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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="userMapper" class="com.gzk.mapper.UserMapperImpl"/>

<!-- 配置被代理類UserServiceImpl -->
<bean id="userService" class="com.gzk.service.UserServiceImpl">
<property name="userMapper" ref="userMapper"/>
</bean>

<!-- 配置代理類Logger -->
<bean id="logger" class="com.gzk.utils.Logger"/>

<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="aspectLogger" ref="logger">
<!-- 配置通知型別,並且建立通知方法和切入點方法的關聯 -->
<!-- 配置前置通知 -->
<aop:before method="beforeLogger" pointcut="execution(* com.gzk.service.*.*(..))"/>
<!-- 配置後置通知 -->
<aop:after-returning method="afterReturningLogger" pointcut="execution(public void com.gzk.service.UserServiceImpl.test())"/>
<!-- 配置異常通知 -->
<aop:after-throwing method="afterThrowingLogger" pointcut="execution(* com.gzk.service.*.*(..))"/>
<!-- 配置最終通知 -->
<aop:after method="afterLogger" pointcut="execution(public void com.gzk.service.UserServiceImpl.test())"/>

</aop:aspect>
</aop:config>

</beans>

5.測試:

public class UserTest {

@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.test();
}

}

6.結果

當發生異常時:

2.2.4、通用化切入點配置

<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="userMapper" class="com.gzk.mapper.UserMapperImpl"/>
<bean id="userService" class="com.gzk.service.UserServiceImpl">
<property name="userMapper" ref="userMapper"/>
</bean>

<!-- 配置Logger類 -->
<bean id="logger" class="com.gzk.utils.Logger"/>

<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="aspectLogger" ref="logger">
<!-- 配置通知型別,並且建立通知方法和切入點方法的關聯 -->
<!-- 配置前置通知 -->
<aop:before method="beforeLogger" pointcut-ref="point-cut"/>
<!-- 配置後置通知 -->
<aop:after-returning method="afterReturningLogger" pointcut-ref="point-cut"/>
<!-- 配置異常通知 -->
<aop:after-throwing method="afterThrowingLogger" pointcut-ref="point-cut"/>
<!-- 配置最終通知 -->
<aop:after method="afterLogger" pointcut-ref="point-cut"/>

<!-- 配置切入點表示式 -->
<!--
注意:
此標籤寫在aop:aspect標籤內部只能當前標籤使用
它還可以寫在aop:aspect標籤外面,則所有切面都可以使用它
若是寫在aop:aspect標籤外面,這必須按照aop的規則和順序約束,要把<aop:poincut>寫在最前面,也就是aop:config下面
-->
<aop:pointcut id="point-cut" expression="execution(* com.gzk.service.*.*(..))"/>

</aop:aspect>
</aop:config>

</beans>

2.2.5、環繞通知

1.將其他通知註釋掉,寫一個環繞通知:

<aop:around method="aroundLogger" pointcut-ref="point-cut"/>

2.在Logger類中加入環繞方法:

public void aroundLogger(){
System.out.println("環繞通知");
}

3.結果:

很明顯出問題了,被代理類中的方法沒了,是什麼問題呢?

  • 問題:

    • 當我們配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了。

  • 分析:

    • 通過對比動態代理中的環繞通知程式碼,發現動態代理的環繞通知有明確的切入點方法呼叫,而我們的程式碼沒有。

  • 解決:

    • spring框架為我們提供了一個介面,ProceedingJoinPoint。該介面有一個方法proceed(),此方法就相當於明確呼叫切入點方法。

    • 該介面可以作為環繞通知的方法引數,在程式執行時,spring框架會為我們提供介面的實現類供我們呼叫。

好了,問題找到且分析了,那我們開始寫程式碼吧

xml配置檔案不用變,但Logger代理類中的aroundLogger()方法就要改變。

//環繞通知
public Object aroundLogger(ProceedingJoinPoint point){
Object[] args = point.getArgs();
Object proceed;
try {
//前置通知
System.out.println("前置通知");
proceed = point.proceed(args);
//後置通知
System.out.println("後置通知");
return proceed;
}catch (Throwable e){
//異常通知
System.out.println("異常通知");
throw new RuntimeException(e);

}finally {
//最終通知
System.out.println("最終通知");
}

}

結果:

2.2.6、測試(註解)

1.通過註解方式隨意配置mapper和service包下的類,用作測試

public interface UserMapper {
void test();
}
@Repository("userMapper")
public class UserMapperImpl implements UserMapper {
public void test() {
System.out.println("測試中。。。");
}
}
public interface UserService {
void test();
}
@Service("userService")
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

public void test() {
userMapper.test();
}
}

2.將UserServiceImpl設為被代理的類

3.新建一個utils包,建立一個Logger類【代理類】(使用註解)

@Component("logger")
@Aspect
public class Logger {

@Pointcut("execution(* com.gzk.service.*.*(..))")
public void pointcut(){}

@Before("pointcut()")
public void beforeLogger(){
System.out.println("前置通知");
}

@AfterReturning("pointcut()")
public void afterReturningLogger(){
System.out.println("後置通知");
}

@AfterThrowing("pointcut()")
public void afterThrowingLogger(){
System.out.println("異常通知");
}

@After("pointcut()")
public void afterLogger(){
System.out.println("最終通知");
}

//環繞通知
@Around("pointcut()")
public Object aroundLogger(ProceedingJoinPoint point){
Object[] args = point.getArgs();
Object proceed;
try {
//前置通知
System.out.println("環繞-前置通知");
proceed = point.proceed(args);
//後置通知
System.out.println("環繞-後置通知");
return proceed;
}catch (Throwable e){
//異常通知
System.out.println("環繞-異常通知");
throw new RuntimeException(e);
}finally {
//最終通知
System.out.println("環繞-最終通知");
}

}

}

4.此時有兩種方式來配置 xml 檔案,一種是xml,一種是java

4.1、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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!-- 配置要掃描的包 -->
<context:component-scan base-package="com.gzk"/>

<!-- 開啟aop自動註解 -->
<aop:aspectj-autoproxy/>

</beans>

4.2、java為配置檔案,建立一個config包,在下面新建一個類

@Configuration
@ComponentScan(basePackages = "com.gzk")
@EnableAspectJAutoProxy
public class ApplicationConfig {
}

5.測試

5.1、使用xml為配置類測試:

public class UserTest {

@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.test();
}

}

5.2、使用java為配置類測試

public class UserTest {

@Test
public void test(){
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.test();
}

}

6.結果: