文章目錄
- 案例①:業務層接口執行效率
- 需求分析
- 環境準備
- 實作步驟
- 案例②:百度網盤密碼資料相容處理
- 需求分析
- 環境準備
- 實作步驟
- 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();
}
}
最終建立好的項目結構如下:
實作步驟
步驟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:運作單元測試類
**注意:**因為程式每次執行的時長是不一樣的,是以運作多次最終的結果是不一樣的。
步驟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:運作單元測試類
補充說明
目前測試的接口執行效率僅僅是一個理論值,并不是一次完整的執行過程。
這塊隻是通過該案例把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);
}
}
最終建立好的項目結構如下:
現在項目的效果是,當輸入密碼為"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:适用于環繞通知
- 擷取切入點方法傳回值,前置和抛出異常後通知是沒有傳回值,後置通知可有可無,是以不做研究
- 傳回後通知
- 環繞通知
- 擷取切入點方法運作異常資訊,前置和傳回後通知是不會有,後置通知可有可無,是以不做研究
- 抛出異常後通知
- 環繞通知