天天看點

[Spring Framework]AOP經典案例、AOP總結

文章目錄

  • ​​案例①:業務層接口執行效率​​
  • ​​需求分析​​
  • ​​環境準備​​
  • ​​實作步驟​​
  • ​​案例②:百度網盤密碼資料相容處理​​
  • ​​需求分析​​
  • ​​環境準備​​
  • ​​實作步驟​​
  • ​​AOP總結​​
  • ​​AOP的核心概念​​
  • ​​切入點表達式​​
  • ​​五種通知類型​​
  • ​​通知中擷取參數​​

案例①:業務層接口執行效率

需求分析

這個需求也比較簡單,前面我們在介紹AOP的時候已經示範過:

  • 需求:任意業務層接口執行均可顯示其執行效率(執行時長)

這個案例的目的是檢視每個業務層執行的時間,這樣就可以監控出哪個業務比較耗時,将其查找出來友善優化。

具體實作的思路:

(1) 開始執行方法之前記錄一個時間

(2) 執行方法

(3) 執行完方法之後記錄一個時間

(4) 用後一個時間減去前一個時間的內插補點,就是我們需要的結果。

是以要在方法執行的前後添加業務,經過分析我們将采用​

​環繞通知​

​。

說明:原始方法如果隻執行一次,時間太快,兩個時間差可能為0,是以我們要執行萬次來計算時間差。

環境準備

  • 建立一個Maven項目
  • pom.xml添加Spring依賴
<dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.4</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>      
  • 添加AccountService、AccountServiceImpl、AccountDao與Account類
public interface AccountService {
    void save(Account account);
    void delete(Integer id);
    void update(Account account);
    List<Account> findAll();
    Account findById(Integer id);
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account){
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List<Account> findAll() {
        return accountDao.findAll();
    }
}
public interface AccountDao {

    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
    //setter..getter..toString方法省略
}      
  • resources下提供一個jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root      
  • 建立相關配置類
//Spring配置類:SpringConfig
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
//JdbcConfig配置類
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
//MybatisConfig配置類
public class MybatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.itheima.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.itheima.dao");
        return msc;
    }
}      
  • 編寫Spring整合Junit的測試類
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
        Account ac = accountService.findById(2);
    }

    @Test
    public void testFindAll(){
        List<Account> all = accountService.findAll();
    }

}      

最終建立好的項目結構如下:

[Spring Framework]AOP經典案例、AOP總結

實作步驟

步驟1:開啟SpringAOP的注解功能

在Spring的主配置檔案SpringConfig類中添加注解

@EnableAspectJAutoProxy      

步驟2:建立AOP的通知類

  • 該類要被Spring管理,需要添加@Component
  • 要辨別該類是一個AOP的切面類,需要添加@Aspect
  • 配置切入點表達式,需要添加一個方法,并添加@Pointcut
@Component
@Aspect
public class ProjectAdvice {
    //配置業務層的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    
    public void runSpeed(){
        
    } 
}      

步驟3:添加環繞通知

在runSpeed()方法上添加@Around

@Component
@Aspect
public class ProjectAdvice {
    //配置業務層的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以簡寫為下面的方式
    @Around("servicePt()")
    public Object runSpeed(ProceedingJoinPoint pjp){
        Object ret = pjp.proceed();
        return ret;
    } 
}      

注意:目前并沒有做任何增強

步驟4:完成核心業務,記錄萬次執行的時間

@Component
@Aspect
public class ProjectAdvice {
    //配置業務層的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以簡寫為下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("業務層接口萬次執行時間: "+(end-start)+"ms");
    } 
}      

步驟5:運作單元測試類

[Spring Framework]AOP經典案例、AOP總結

**注意:**因為程式每次執行的時長是不一樣的,是以運作多次最終的結果是不一樣的。

步驟6:程式優化

目前程式所面臨的問題是,多個方法一起執行測試的時候,控制台都列印的是:

​業務層接口萬次執行時間:xxxms​

我們沒有辦法區分到底是哪個接口的哪個方法執行的具體時間,具體如何優化?

@Component
@Aspect
public class ProjectAdvice {
    //配置業務層的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以簡寫為下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        //擷取執行簽名資訊
        Signature signature = pjp.getSignature();
        //通過簽名擷取執行操作名稱(接口名)
        String className = signature.getDeclaringTypeName();
        //通過簽名擷取執行操作名稱(方法名)
        String methodName = signature.getName();
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("萬次執行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    } 
}      

步驟7:運作單元測試類

[Spring Framework]AOP經典案例、AOP總結

補充說明

目前測試的接口執行效率僅僅是一個理論值,并不是一次完整的執行過程。

這塊隻是通過該案例把AOP的使用進行了學習,具體的實際值是有很多因素共同決定的。

案例②:百度網盤密碼資料相容處理

需求分析

問題描述:

  • 當我們從别人發給我們的内容中複制提取碼的時候,有時候會多複制到一些空格,直接粘貼到百度的提取碼輸入框
  • 但是百度那邊記錄的提取碼是沒有空格的
  • 這個時候如果不做處理,直接對比的話,就會引發提取碼不一緻,導緻無法通路百度盤上的内容
  • 是以多輸入一個空格可能會導緻項目的功能無法正常使用。
  • 此時我們就想能不能将輸入的參數先幫使用者去掉空格再操作呢?

答案是可以的,我們隻需要在業務方法執行之前對所有的輸入參數進行格式處理——trim()

  • 是對所有的參數都需要去除空格麼?

也沒有必要,一般隻需要針對字元串處理即可。

  • 以後涉及到需要去除前後空格的業務可能會有很多,這個去空格的代碼是每個業務都寫麼?

可以考慮使用AOP來統一處理。

  • AOP有五種通知類型,該使用哪種呢?

我們的需求是将原始方法的參數處理後在參與原始方法的調用,能做這件事的就隻有環繞通知。

綜上所述,我們需要考慮兩件事:

①:在業務方法執行之前對所有的輸入參數進行格式處理——trim()

②:使用處理後的參數調用原始方法——環繞通知中存在對原始方法的調用

環境準備

  • 建立一個Maven項目
  • pom.xml添加Spring依賴
<dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.4</version>
    </dependency>
  </dependencies>      
  • 添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl類
public interface ResourcesDao {
    boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
        //模拟校驗
        return password.equals("root");
    }
}
public interface ResourcesService {
    public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
    @Autowired
    private ResourcesDao resourcesDao;

    public boolean openURL(String url, String password) {
        return resourcesDao.readResources(url,password);
    }
}      
  • 建立Spring的配置類
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}      
  • 編寫App運作類
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
        boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
        System.out.println(flag);
    }
}      

最終建立好的項目結構如下:

[Spring Framework]AOP經典案例、AOP總結

現在項目的效果是,當輸入密碼為"root"控制台列印為true,如果密碼改為"root "控制台列印的是false

需求是使用AOP将參數進行統一處理,不管輸入的密碼​

​root​

​前後包含多少個空格,最終控制台列印的都是true。

實作步驟

步驟1:開啟SpringAOP的注解功能

@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}      

步驟2:編寫通知類

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
}      

步驟3:添加環繞通知

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")這兩種寫法都對
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        return ret;
    }
    
}      

步驟4:完成核心業務,處理參數中的空格

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")這兩種寫法都對
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        //擷取原始方法的參數
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            //判斷參數是不是字元串
            if(args[i].getClass().equals(String.class)){
                args[i] = args[i].toString().trim();
            }
        }
        //将修改後的參數傳入到原始方法的執行中
        Object ret = pjp.proceed(args);
        return ret;
    }
    
}      

步驟5:運作程式

不管密碼​

​root​

​前後是否加空格,最終控制台列印的都是true

步驟6:優化測試

為了能更好的看出AOP已經生效,我們可以修改ResourcesImpl類,在方法中将密碼的長度進行列印

@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
        System.out.println(password.length());
        //模拟校驗
        return password.equals("root");
    }
}      

再次運作成功,就可以根據最終列印的長度來看看,字元串的空格有沒有被去除掉。

AOP總結

AOP的核心概念

  • 概念:AOP(Aspect Oriented Programming)面向切面程式設計,一種程式設計範式
  • 作用:在不驚動原始設計的基礎上為方法進行功能增強
  • 核心概念
  • 代理(Proxy):SpringAOP的核心本質是采用代理模式實作的
  • 連接配接點(JoinPoint):在SpringAOP中,了解為任意方法的執行
  • 切入點(Pointcut):比對連接配接點的式子,也是具有共性功能的方法描述
  • 通知(Advice):若幹個方法的共性功能,在切入點處執行,最終展現為一個方法
  • 切面(Aspect):描述通知與切入點的對應關系
  • 目标對象(Target):被代理的原始對象成為目标對象

切入點表達式

  • 切入點表達式标準格式:動作關鍵字(通路修飾符 傳回值 包名.類/接口名.方法名(參數)異常名)
execution(* com.itheima.service.*Service.*(..))      
  • 切入點表達式描述通配符:
  • 作用:用于快速描述,範圍描述
  • ​*​

    ​:比對任意符号(常用)
  • ​..​

    ​ :比對多個連續的任意符号(常用)
  • ​+​

    ​:比對子類類型
  • 切入點表達式書寫技巧

    1.按标準規範開發

    2.查詢操作的傳回值建議使用*比對

    3.減少使用…的形式描述包

    4.對接口進行描述,使用*表示子產品名,例如UserService的比對描述為*Service

    5.方法名書寫保留動詞,例如get,使用*表示名詞,例如getById比對描述為getBy*

    6.參數根據實際情況靈活調整

五種通知類型

  • 前置通知
  • 後置通知
  • 環繞通知(重點)
  • 環繞通知依賴形參ProceedingJoinPoint才能實作對原始方法的調用
  • 環繞通知可以隔離原始方法的調用執行
  • 環繞通知傳回值設定為Object類型
  • 環繞通知中可以對原始方法調用過程中出現的異常進行處理
  • 傳回後通知
  • 抛出異常後通知

通知中擷取參數

  • 擷取切入點方法的參數,所有的通知類型都可以擷取參數
  • JoinPoint:适用于前置、後置、傳回後、抛出異常後通知
  • ProceedingJoinPoint:适用于環繞通知
  • 擷取切入點方法傳回值,前置和抛出異常後通知是沒有傳回值,後置通知可有可無,是以不做研究
  • 傳回後通知
  • 環繞通知
  • 擷取切入點方法運作異常資訊,前置和傳回後通知是不會有,後置通知可有可無,是以不做研究
  • 抛出異常後通知
  • 環繞通知