spring筆記02

[toc]

代碼冗余與裝飾器模式

代碼冗余現(xiàn)象

我們的Service層實(shí)現(xiàn)類中的每個(gè)方法都要加上事務(wù)控制,這樣使得每個(gè)方法的前后都要加上重復(fù)的事務(wù)控制的代碼,如下:

@Override
public void saveAccount(Account account) {
    try {
        TransactionManager.beginTransaction();
        accountDao.save(account);       // 唯一的一行業(yè)務(wù)代碼
        TransactionManager.commit();
    } catch (Exception e) {
        TransactionManager.rollback();
        e.printStackTrace();
    }finally {
        TransactionManager.release();
    }
}

@Override
public void updateAccount(Account account) {
    try {
        TransactionManager.beginTransaction();
        accountDao.update(account);     // 唯一的一行業(yè)務(wù)代碼
        TransactionManager.commit();
    } catch (Exception e) {
        TransactionManager.rollback();
        e.printStackTrace();
    }finally {
        TransactionManager.release();
    }
}

@Override
public void deleteAccount(Integer accountId) {
    try {
        TransactionManager.beginTransaction();
        accountDao.delete(accountId);   // 唯一的一行業(yè)務(wù)代碼
        TransactionManager.commit();
    } catch (Exception e) {
        TransactionManager.rollback();
        e.printStackTrace();
    }finally {
        TransactionManager.release();
    }
}

我們發(fā)現(xiàn)出現(xiàn)了兩個(gè)問題:

  1. 業(yè)務(wù)層方法變得臃腫了,里面充斥著很多重復(fù)代碼.
  2. 業(yè)務(wù)層方法和事務(wù)控制方法耦合了. 若提交,回滾,釋放資源中任何一個(gè)方法名變更,都需要修改業(yè)務(wù)層的代碼.

因此我們引入了裝飾模式解決代碼冗余和耦合現(xiàn)象.

解決代碼冗余的思路: 裝飾模式和動(dòng)態(tài)代理

動(dòng)態(tài)代理的寫法

常用的動(dòng)態(tài)代理分為兩種

  1. 基于接口的動(dòng)態(tài)代理,使用JDK 官方的 Proxy 類,要求被代理者至少實(shí)現(xiàn)一個(gè)接口
  2. 基于子類的動(dòng)態(tài)代理,使用第三方的 CGLib庫,要求被代理類不能是final類.

在這里我們使用基于接口的動(dòng)態(tài)代理,寫法如下:

接口名 新對(duì)象名 = (接口名)Proxy.newProxyInstance(
    被代理的對(duì)象.getClass().getClassLoader(), // 被代理對(duì)象的類加載器,固定寫法
    被代理的對(duì)象.getClass().getInterfaces(),  // 被代理對(duì)象實(shí)現(xiàn)的所有接口,固定寫法
    new InvocationHandler() {   // 匿名內(nèi)部類,通過攔截被代理對(duì)象的方法來增強(qiáng)被代理對(duì)象
        /* 被代理對(duì)象的任何方法執(zhí)行時(shí),都會(huì)被此方法攔截到
            其參數(shù)如下:
                proxy: 代理對(duì)象的引用,不一定每次都用得到
                method: 被攔截到的方法對(duì)象
                args: 被攔截到的方法對(duì)象的參數(shù)
            返回值:
                被增強(qiáng)后的返回值
        */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if("方法名".equals(method.getName())) {
                // 增強(qiáng)方法的操作
                rtValue = method.invoke(被代理的對(duì)象, args);
                // 增強(qiáng)方法的操作
                return rtValue;
            }          
        }
    });

使用動(dòng)態(tài)代理解決代碼冗余現(xiàn)象

我們使用動(dòng)態(tài)代理對(duì)上述Service進(jìn)行改造,創(chuàng)建BeanFactory類作為service層對(duì)象工廠,通過其getAccountService方法得到業(yè)務(wù)層對(duì)象.

// 用于創(chuàng)建Service的代理對(duì)象的工廠
public class BeanFactory {

    private IAccountService accountService;     // 被增強(qiáng)的service對(duì)象
    private TransactionManager txManager;       // 事務(wù)控制工具類

    // 成員變量的set方法,以便Spring容器注入
    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }
    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    // 獲取增強(qiáng)后的Service對(duì)象
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
            accountService.getClass().getInterfaces(),
            new InvocationHandler() {
                // 增強(qiáng)方法
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object rtValue = null;
                    try {
                        //1.開啟事務(wù)
                        txManager.beginTransaction();
                        //2.執(zhí)行操作
                        rtValue = method.invoke(accountService, args);
                        //3.提交事務(wù)
                        txManager.commit();
                        //4.返回結(jié)果
                        return rtValue;
                    } catch (Exception e) {
                        //5.回滾操作
                        txManager.rollback();
                        throw new RuntimeException(e);
                    } finally {
                        //6.釋放連接
                        txManager.release();
                    }
                }
            });
    }
}

bean.xml中,添加如下配置

<!--配置beanfactory-->
<bean id="beanFactory" class="cn.maoritian.factory.BeanFactory">
    <!-- 注入service -->
    <property name="accountService" ref="accountService"></property>
    <!-- 注入事務(wù)控制工具 -->
    <property name="txManager" ref="txManager"></property>
</bean>

這樣,我們就可以通過Spring的IOC獲取增強(qiáng)后的Service對(duì)象.

使用SpringAOP解決代碼冗余

AOP相關(guān)術(shù)語

  • Joinpoint(連接點(diǎn)): 被攔截到的方法.

  • Pointcut(切入點(diǎn)): 我們對(duì)其進(jìn)行增強(qiáng)的方法.

  • Advice(通知/增強(qiáng)): 對(duì)切入點(diǎn)進(jìn)行的增強(qiáng)操作

    包括前置通知,后置通知,異常通知,最終通知,環(huán)繞通知

  • Weaving(織入): 是指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程。

  • Aspect(切面): 是切入點(diǎn)和通知的結(jié)合

使用XML配置AOP

使用XML配置AOP的步驟

bean.xml中配置AOP要經(jīng)過以下幾步:

  1. bean.xml中引入約束并將通知類注入Spring容器中

    <?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
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd">
        
        <!--通知類-->
        <bean id="logger" class="cn.maoritian.utils.Logger"></bean>
    </beans>
    
  2. 使用<aop:config>標(biāo)簽聲明AOP配置,所有關(guān)于AOP配置的代碼都寫在<aop:config>標(biāo)簽內(nèi)

    <aop:config>
        <!-- AOP配置的代碼都寫在此處 -->
    </aop:config>
    
  3. 使用<aop:aspect>標(biāo)簽配置切面,其屬性如下

    • id: 指定切面的id

    • ref: 引用通知類的id

      <aop:config>
          <aop:aspect id="logAdvice" ref="logger">
              <!--配置通知的類型要寫在此處-->
          </aop:aspect>
      </aop:config>
      
  4. 使用<aop:pointcut>標(biāo)簽配置切入點(diǎn)表達(dá)式,指定對(duì)哪些方法進(jìn)行增強(qiáng),其屬性如下

    • id: 指定切入點(diǎn)表達(dá)式的id

    • expression: 指定切入點(diǎn)表達(dá)式

      <aop:config>
          <aop:aspect id="logAdvice" ref="logger">
              <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
          </aop:aspect>
      </aop:config>
      
  1. 使用<aop:xxx>標(biāo)簽配置對(duì)應(yīng)類型的通知方法

    • 其屬性如下:

      • method: 指定通知類中的增強(qiáng)方法名.
      • ponitcut-ref: 指定切入點(diǎn)的表達(dá)式的id
      • poinitcut: 指定切入點(diǎn)表達(dá)式

      其中pointcut-refpointref屬性只能有其中一個(gè)

    • 具體的通知類型:

      • <aop:before>: 配置前置通知,指定的增強(qiáng)方法在切入點(diǎn)方法之前執(zhí)行.
      • <aop:after-returning>: 配置后置通知,指定的增強(qiáng)方法在切入點(diǎn)方法正常執(zhí)行之后執(zhí)行.
      • <aop:after-throwing>: 配置異常通知,指定的增強(qiáng)方法在切入點(diǎn)方法產(chǎn)生異常后執(zhí)行.
      • <aop:after>: 配置最終通知,無論切入點(diǎn)方法執(zhí)行時(shí)是否發(fā)生異常,指定的增強(qiáng)方法都會(huì)最后執(zhí)行.
      • <aop:around>: 配置環(huán)繞通知,可以在代碼中手動(dòng)控制增強(qiáng)代碼的執(zhí)行時(shí)機(jī).
      <aop:config>
          <aop:aspect id="logAdvice" ref="logger">
              <!--指定切入點(diǎn)表達(dá)式-->
              <aop:pointcut expression="execution(* cn,maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
              <!--配置各種類型的通知-->
              <aop:before method="printLogBefore" pointcut-ref="pt1"></aop:before>
              <aop:after-returning method="printLogAfterReturning" pointcut-ref="pt1"></aop:after-returning>
              <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="pt1"></aop:after-throwing>
              <aop:after method="printLogAfter" pointcut-ref="pt1"></aop:after>
              <!--環(huán)繞通知一般單獨(dú)使用-->       
              <!-- <aop:around method="printLogAround" pointcut-ref="pt1"></aop:around> -->
          </aop:aspect>
      </aop:config>
      

切入點(diǎn)表達(dá)式

  • 切入點(diǎn)表達(dá)式的寫法: execution([修飾符] 返回值類型 包路徑.類名.方法名(參數(shù)))

  • 切入點(diǎn)表達(dá)式的省略寫法:

    • 全匹配方式:

      <aop:pointcut expression="execution(public void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 其中訪問修飾符可以省略:

      <aop:pointcut expression="execution(void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 返回值可使用*,表示任意返回值:

      <aop:pointcut expression="execution(* cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 包路徑可以使用*,表示任意包. 但是*.的個(gè)數(shù)要和包的層級(jí)數(shù)相匹配

      <aop:pointcut expression="execution(*  *.*.*.*.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 包路徑可以使用*..,表示當(dāng)前包,及其子包(因?yàn)楸纠又袑?code>bean.xml放在根路徑下,因此..可以匹配項(xiàng)目?jī)?nèi)所有包路徑)

      <aop:pointcut expression="execution(*  *..AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 類名可以使用*,表示任意類

      <aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
      
    • 方法名可以使用*,表示任意方法

      <aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
      
    • 參數(shù)列表可以使用*,表示參數(shù)可以是任意數(shù)據(jù)類型,但是必須存在參數(shù)

      <aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut>
      
    • 參數(shù)列表可以使用..表示有無參數(shù)均可,有參數(shù)可以是任意類型

      <aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
      
    • 全通配方式,可以匹配匹配任意方法

      <aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
      
  • 切入點(diǎn)表達(dá)式的一般寫法

    一般我們都是對(duì)業(yè)務(wù)層所有實(shí)現(xiàn)類的所有方法進(jìn)行增強(qiáng),因此切入點(diǎn)表達(dá)式寫法通常為

    <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
    

環(huán)繞通知

  1. 前置通知,后置通知,異常通知,最終通知的執(zhí)行順序

    Spring是基于動(dòng)態(tài)代理對(duì)方法進(jìn)行增強(qiáng)的,前置通知,后置通知,異常通知,最終通知在增強(qiáng)方法中的執(zhí)行時(shí)機(jī)如下:

    // 增強(qiáng)方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        Object rtValue = null;
        try {
            // 執(zhí)行前置通知
            
            // 執(zhí)行原方法
            rtValue = method.invoke(accountService, args); 
            
            // 執(zhí)行后置通知
            return rtValue;
        } catch (Exception e) {
            // 執(zhí)行異常通知
        } finally {
            // 執(zhí)行最終通知
        }
    }
    
  2. 環(huán)繞通知允許我們更自由地控制增強(qiáng)代碼執(zhí)行的時(shí)機(jī)

    Spring框架為我們提供一個(gè)接口ProceedingJoinPoint,它的實(shí)例對(duì)象可以作為環(huán)繞通知方法的參數(shù),通過參數(shù)控制被增強(qiáng)方法的執(zhí)行時(shí)機(jī)。

    • ProceedingJoinPoint對(duì)象的getArgs()方法返回被攔截的參數(shù)
    • ProceedingJoinPoint對(duì)象的proceed()方法執(zhí)行被攔截的方法
    // 環(huán)繞通知方法,返回Object類型
    public Object printLogAround(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();        
            printLogBefore();           // 執(zhí)行前置通知
            rtValue = pjp.proceed(args);// 執(zhí)行被攔截方法
            printLogAfterReturn();      // 執(zhí)行后置通知
        }catch(Throwable e) {
            printLogAfterThrowing();    // 執(zhí)行異常通知
        }finally {
            printLogAfter();            // 執(zhí)行最終通知
        }
        return rtValue;
    }
    

使用注解配置AOP

半注解配置AOP

Spring注解配置AOP的步驟

半注解配置AOP,需要在bean,xml中加入下面語句開啟對(duì)注解AOP的支持

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

Spring用于AOP的注解

用于聲明切面的注解

@Aspect: 聲明當(dāng)前類為通知類,該類定義了一個(gè)切面.相當(dāng)于xml配置中的<aop:aspect>標(biāo)簽

@Component("logger")
@Aspect
public class Logger {
    // ...
}

用于聲明通知的注解

  • @Before: 聲明該方法為前置通知.相當(dāng)于xml配置中的<aop:before>標(biāo)簽
  • @AfterReturning: 聲明該方法為后置通知.相當(dāng)于xml配置中的<aop:after-returning>標(biāo)簽
  • @AfterThrowing: 聲明該方法為異常通知.相當(dāng)于xml配置中的<aop:after-throwing>標(biāo)簽
  • @After: 聲明該方法為最終通知.相當(dāng)于xml配置中的<aop:after>標(biāo)簽
  • @Around: 聲明該方法為環(huán)繞通知.相當(dāng)于xml配置中的<aop:around>標(biāo)簽

屬性:

  • value: 用于指定切入點(diǎn)表達(dá)式切入點(diǎn)表達(dá)式的引用
@Component("logger")
@Aspect //表示當(dāng)前類是一個(gè)通知類
public class Logger {

    // 配置前置通知
    @Before("execution(* cn.maoritian.service.impl.*.*(..))")
    public void printLogBefore(){
        System.out.println("前置通知Logger類中的printLogBefore方法開始記錄日志了。。。");
    }

    // 配置后置通知
    @AfterReturning("execution(* cn.maoritian.service.impl.*.*(..))")
    public void printLogAfterReturning(){
        System.out.println("后置通知Logger類中的printLogAfterReturning方法開始記錄日志了。。。");
    }
    
    // 配置異常通知
    @AfterThrowing("execution(* cn.maoritian.service.impl.*.*(..))")
    public void printLogAfterThrowing(){
        System.out.println("異常通知Logger類中的printLogAfterThrowing方法開始記錄日志了。。。");
    }

    // 配置最終通知
    @After("execution(* cn.maoritian.service.impl.*.*(..))")
    public void printLogAfter(){
        System.out.println("最終通知Logger類中的printLogAfter方法開始記錄日志了。。。");
    }

    // 配置環(huán)繞通知
    @Around("execution(* cn.maoritian.service.impl.*.*(..))")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();  
            printLogBefore();               // 執(zhí)行前置通知
            rtValue = pjp.proceed(args);    // 執(zhí)行切入點(diǎn)方法
            printLogAfterReturning();       // 執(zhí)行后置通知
            return rtValue;
        }catch (Throwable t){
            printLogAfterThrowing();        // 執(zhí)行異常通知
            throw new RuntimeException(t);
        }finally {
            printLogAfter();                // 執(zhí)行最終通知
        }
    }
}

用于指定切入點(diǎn)表達(dá)式的注解

@Pointcut: 指定切入點(diǎn)表達(dá)式,其屬性如下:

  • value: 指定表達(dá)式的內(nèi)容

@Pointcut注解沒有id屬性,通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式.

@Component("logger")
@Aspect //表示當(dāng)前類是一個(gè)通知類
public class Logger {

    // 配置切入點(diǎn)表達(dá)式
    @Pointcut("execution(* cn.maoritian.service.impl.*.*(..))")
    private void pt1(){} 
    
    // 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
    @Before("pt1()")
    public void printLogBefore(){
        System.out.println("前置通知Logger類中的printLogBefore方法開始記錄日志了。。。");
    }

    // 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
    @AfterReturning("pt1()")
    public void printLogAfterReturning(){
        System.out.println("后置通知Logger類中的printLogAfterReturning方法開始記錄日志了。。。");
    }
    
    // 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
    @AfterThrowing("pt1()")
    public void printLogAfterThrowing(){
        System.out.println("異常通知Logger類中的printLogAfterThrowing方法開始記錄日志了。。。");
    }
}

純注解配置AOP

在Spring配置類前添加@EnableAspectJAutoProxy注解,可以使用純注解方式配置AOP

@Configuration
@ComponentScan(basePackages="cn.maoritian")
@EnableAspectJAutoProxy         // 允許AOP
public class SpringConfiguration {
    // 具體配置
    //...
}

使用注解配置AOP的bug

在使用注解配置AOP時(shí),會(huì)出現(xiàn)一個(gè)bug. 四個(gè)通知的調(diào)用順序依次是:前置通知,最終通知,后置通知. 這會(huì)導(dǎo)致一些資源在執(zhí)行最終通知時(shí)提前被釋放掉了,而執(zhí)行后置通知時(shí)就會(huì)出錯(cuò)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容