14. AOP配置管理

2023-12-01 10:22:49 浏览数 (1)

1. AOP 切入点表达式

前面的案例中,有涉及到如下内容:

对于 AOP 中切入点表达式,我们总共会学习三个内容,分别是语法格式​、通配符​ 和书写技巧​。

1.1 语法格式

首先我们先要明确两个概念:

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

对于切入点的描述,我们其实是有两中方式的,先来看下前面的例子

描述方式一:执行 com.itheima.dao 包下的 BookDao 接口中的无参数 update 方法

代码语言:javascript复制
execution(void com.itheima.dao.BookDao.update())

描述方式二:执行 com.itheima.dao.impl 包下的 BookDaoImpl 类中的无参数 update 方法

代码语言:javascript复制
execution(void com.itheima.dao.impl.BookDaoImpl.update())

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。

对于切入点表达式的语法为:

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

对于这个格式,我们不需要硬记,通过一个例子,理解它:

代码语言:javascript复制
execution(public User com.itheima.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如 execution 表示执行到指定切入点
  • public:访问修饰符,还可以是 public,private 等,可以省略
  • User:返回值,写返回值类型
  • com.itheima.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想这块就会觉得将来编写起来会比较麻烦,有没有更简单的方式呢?

就需要用到下面所学习的通配符。

1.2 通配符

我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?

*​:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

代码语言:javascript复制
execution(public * com.itheima.*.UserService.find*(*))

匹配 com.itheima 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法

..​:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

代码语言:javascript复制
execution(public User com..UserService.findById(..))

匹配 com 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法

​:专用于匹配子类类型

代码语言:javascript复制
execution(* *..*Service .*(..))

这个使用率较低,描述子类的,咱们做 JavaEE 开发,继承机会就一次,使用都很慎重,所以很少用它。*Service ,表示所有以 Service 结尾的接口的子类。

接下来,我们把案例中使用到的切入点表达式来分析下:

代码语言:javascript复制
execution(void com.itheima.dao.BookDao.update())
匹配接口,能匹配到

execution(void com.itheima.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到

execution(* com.itheima.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到

execution(* com.itheima.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数

execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配

execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配

execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配

execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广

execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配

execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配

execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法

execution(* com.itheima.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配

execution(* com.itheima.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

后面两种更符合我们平常切入点表达式的编写规则

1.3 书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用 public 描述(访问控制修饰符针对接口开发均采用 public 描述(可省略访问控制修饰符描述
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  • 包名书写书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的书写名称与模块相关的采用*匹配,例如 UserService 书写成*Service,绑定业务层接口名,例如 UserService 书写成*Service,绑定业务层接口名
  • 方法名书写以书写以动词进行进行精准匹配,名词采用,名词采用匹配,例如 getById 书写成 getBy,selectAll 书写成 selectAll,selectAll 书写成 selectAll
  • 参数规则较为复杂,根据业务方法灵活调整参数规则较为复杂,根据业务方法灵活调整
  • 通常通常*不使用异常*作为​匹配​*规则*规则

2. AOP 通知类型

前面的案例中,有涉及到如下内容:

它所代表的含义是将通知​ 添加到切入点​ 方法执行的前面。

除了这个注解外,还有没有其他的注解,换个问题就是除了可以在前面加,能不能在其他的地方加

2.1 类型介绍

我们先来回顾下 AOP 通知:

  • AOP 通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置通知具体要添加到切入点的哪里?

共提供了 5 种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)

为了更好的理解这几种通知类型,我们来看一张图

(1)前置通知,追加功能到方法执行前,类似于在代码 1 或者代码 2 添加内容

(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码 5 添加内容

(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码 3 添加内容,如果方法执行抛出异常,返回后通知将不会被添加

(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码 4 添加内容,只有方法抛出异常后才会被添加

(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,需要我们往下学习。

2.2 环境准备

创建一个 Maven 项目

pom.xml 添加 Spring 依赖

代码语言:javascript复制
<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>

添加 BookDao 和 BookDaoImpl 类

代码语言:javascript复制
public interface BookDao {
    public void update();
    public int select();
}

@Repository
public class BookDaoImpl implements BookDao {
    public void update(){
        System.out.println("book dao update ...");
    }
    public int select() {
        System.out.println("book dao select is running ...");
        return 100;
    }
}

创建 Spring 的配置类

代码语言:javascript复制
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

创建通知类

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    public void before() {
        System.out.println("before advice ...");
    }

    public void after() {
        System.out.println("after advice ...");
    }

    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }

    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }

    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

编写 App 运行类

代码语言:javascript复制
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

最终创建好的项目结构如下:

2.3 通知类型的使用

前置通知

修改 MyAdvice,在 before 方法上添加@Before注解

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()"),不建议
    public void before() {
        System.out.println("before advice ...");
    }
}

后置通知

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
}

环绕通知

基本使用:

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体如何实现?

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

说明:proceed()为什么要抛出异常?

原因很简单,看下源码就知道了

再次运行,程序可以看到原始方法已经被执行了

注意事项:

(1)原始方法有返回值的处理

  • 修改 MyAdvice,对 BookDao 中的 select 方法添加环绕通知,
代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}

    @Around("pt2()")
    public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}
  • 修改 App 类,调用 select 方法
代码语言:javascript复制
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        int num = bookDao.select();
        System.out.println(num);
    }
}

运行后会报错,错误内容为:

Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.itheima.dao.BookDao.select() at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:226) at com.sun.proxy.$Proxy19.select(Unknown Source) at com.itheima.App.main(App.java:12)

错误大概的意思是:空的返回不匹配原始方法的int返回

  • void 就是返回 Null
  • 原始方法就是 BookDao 下的 select 方法

所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}

    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

说明:

为什么返回的是 Object 而不是 int 的主要原因是 Object 类型更通用。

在环绕通知中是可以对原始方法返回值就行修改的。

返回后通知

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}

    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

注意:返回后通知是需要在原始方法select​ 正常执行后才会被执行,如果select()​ 方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。这个案例大家下去可以自己练习验证下。

异常后通知

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}

    @AfterReturning("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

注意:异常后通知是需要原始方法抛出异常,可以在select()​ 方法中添加一行代码int i = 1/0​ 即可。如果没有抛异常,异常后通知将不会被执行。

学习完这 5 种通知类型,我们来思考下环绕通知是如何实现其他通知类型的功能的?

因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能,如:

通知类型总结

知识点 1:@After

名称

@After

类型

方法注解

位置

通知方法定义上方

作用

设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

知识点 2:@AfterReturning

名称

@AfterReturning

类型

方法注解

位置

通知方法定义上方

作用

设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

知识点 3:@AfterThrowing

名称

@AfterThrowing

类型

方法注解

位置

通知方法定义上方

作用

设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

知识点 4:@Around

名称

@Around

类型

方法注解

位置

通知方法定义上方

作用

设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

环绕通知注意事项

  1. 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好设定为 Object 类型
  4. 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异

3. 业务层接口执行效率

3.1 需求分析

这个需求也比较简单,前面我们在介绍 AOP 的时候已经演示过:

  • 需求:任意业务层接口执行均可显示其执行效率(执行时长)

这个案例的目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来方便优化。

具体实现的思路:

(1) 开始执行方法之前记录一个时间

(2) 执行方法

(3) 执行完方法之后记录一个时间

(4) 用后一个时间减去前一个时间的差值,就是我们需要的结果。

所以要在方法执行的前后添加业务,经过分析我们将采用环绕通知​。

说明:原始方法如果只执行一次,时间太快,两个时间差可能为 0,所以我们要执行万次来计算时间差。

3.2 环境准备

创建一个 Maven 项目

pom.xml 添加 Spring 依赖

代码语言:javascript复制
<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 类

代码语言:javascript复制
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

代码语言:javascript复制
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

创建相关配置类

代码语言:javascript复制
//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 的测试类

代码语言:javascript复制
@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();
    }

}

最终创建好的项目结构如下:

3.3 环境准备

步骤 1:开启 SpringAOP 的注解功能

在 Spring 的主配置文件 SpringConfig 类中添加注解

代码语言:javascript复制
@EnableAspectJAutoProxy
步骤 2:创建 AOP 的通知类
  • 该类要被 Spring 管理,需要添加@Component
  • 要标识该类是一个 AOP 的切面类,需要添加@Aspect
  • 配置切入点表达式,需要添加一个方法,并添加@Pointcut
代码语言:javascript复制
@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}

    public void runSpeed(){

    }
}
步骤 3:添加环绕通知

在 runSpeed()方法上添加@Around

代码语言:javascript复制
@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:完成核心业务,记录万次执行的时间
代码语言:javascript复制
@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

我们没有办法区分到底是哪个接口的哪个方法执行的具体时间,具体如何优化?

代码语言:javascript复制
@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 的使用进行了学习,具体的实际值是有很多因素共同决定的。

4. AOP 通知获取数据

目前我们写 AOP 仅仅是在原始方法前后追加一些操作,接下来我们要说说 AOP 中数据相关的内容,我们将从获取参数​、获取返回值​ 和获取异常​ 三个方面来研究切入点的相关信息。

前面我们介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗?

我们先来一个个分析下:

  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
    • 抛出异常后通知
    • 环绕通知

4.1 环境准备

创建一个 Maven 项目

pom.xml 添加 Spring 依赖

代码语言:javascript复制
<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>

添加 BookDao 和 BookDaoImpl 类

代码语言:javascript复制
public interface BookDao {
    public String findName(int id);
}
@Repository
public class BookDaoImpl implements BookDao {

    public String findName(int id) {
        System.out.println("id:" id);
        return "itcast";
    }
}

创建 Spring 的配置类

代码语言:javascript复制
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

编写通知类

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before() {
        System.out.println("before advice ..." );
    }

    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }

    @Around("pt()")
    public Object around() throws Throwable{
        Object ret = pjp.proceed();
        return ret;
    }
    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }


    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

编写 App 运行类

代码语言:javascript复制
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100);
        System.out.println(name);
    }
}

4.2 获取参数

非环绕通知获取方式

在方法上添加 JoinPoint,通过 JoinPoint 来获取参数

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint jp)
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }
	//...其他的略
}

思考:方法的参数只有一个,为什么获取的是一个数组?

因为参数的个数是不固定的,所以使用数组更通配些。

如果将参数改成两个会是什么效果呢?

(1)修改 BookDao 接口和 BookDaoImpl 实现类

代码语言:javascript复制
public interface BookDao {
    public String findName(int id,String password);
}
@Repository
public class BookDaoImpl implements BookDao {

    public String findName(int id,String password) {
        System.out.println("id:" id);
        return "itcast";
    }
}

(2)修改 App 类,调用方法传入多个参数

代码语言:javascript复制
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100,"itheima");
        System.out.println(name);
    }
}

说明:

使用 JoinPoint 的方式获取参数适用于前置​、后置​、返回后​、抛出异常后​ 通知。剩下的大家自行去验证。

环绕通知获取方式

环绕通知使用的是 ProceedingJoinPoint,因为 ProceedingJoinPoint 是 JoinPoint 类的子类,所以对于 ProceedingJoinPoint 类中应该也会有对应的getArgs()​ 方法,我们去验证下:

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp)throws Throwable {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        Object ret = pjp.proceed();
        return ret;
    }
	//其他的略
}

注意:

pjp.proceed()方法是有两个构造方法,分别是:

调用无参数的 proceed,当原始方法有参数,会在调用的过程中自动传入参数

所以调用这两个方法的任意一个都可以完成功能

但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
        return ret;
    }
	//其他的略
}

有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

4.3 获取返回值

对于返回值,只有返回后AfterReturing​ 和环绕Around​ 这两个通知类型可以获取,具体如何获取?

环绕通知获取返回值
代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
        return ret;
    }
	//其他的略
}

上述代码中,ret​ 就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。

返回后通知获取返回值
代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..." ret);
    }
	//其他的略
}

注意:

(1)参数名的问题

(2)afterReturning 方法参数类型的问题

参数类型可以写成 String,但是为了能匹配更多的参数类型,建议写成 Object 类型

(3)afterReturning 方法参数的顺序问题

运行 App 后查看运行结果,说明返回值已经被获取到

4.4 获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing​ 和环绕Around​ 这两个通知类型可以获取,具体如何获取?

环绕通知获取异常

这块比较简单,以前我们是抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了

代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }
	//其他的略
}

在 catch 方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务需求有关了。

抛出异常后通知获取异常
代码语言:javascript复制
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..." t);
    }
	//其他的略
}

如何让原始方法抛出异常,方式有很多,

代码语言:javascript复制
@Repository
public class BookDaoImpl implements BookDao {

    public String findName(int id,String password) {
        System.out.println("id:" id);
        if(true){
            throw new NullPointerException();
        }
        return "itcast";
    }
}

注意:

5. 百度网盘密码数据兼容处理

5.1 需求分析

需求: 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。

问题描述:

  • 点击链接,会提示,请输入提取码,如下图所示

  • 当我们从别人发给我们的内容中复制提取码的时候,有时候会多复制到一些空格,直接粘贴到百度的提取码输入框
  • 但是百度那边记录的提取码是没有空格的
  • 这个时候如果不做处理,直接对比的话,就会引发提取码不一致,导致无法访问百度盘上的内容
  • 所以多输入一个空格可能会导致项目的功能无法正常使用。
  • 此时我们就想能不能将输入的参数先帮用户去掉空格再操作呢?

答案是可以的,我们只需要在业务方法执行之前对所有的输入参数进行格式处理——trim()

  • 是对所有的参数都需要去除空格么?

也没有必要,一般只需要针对字符串处理即可。

  • 以后涉及到需要去除前后空格的业务可能会有很多,这个去空格的代码是每个业务都写么?

可以考虑使用 AOP 来统一处理。

  • AOP 有五种通知类型,该使用哪种呢?

我们的需求是将原始方法的参数处理后在参与原始方法的调用,能做这件事的就只有环绕通知。

综上所述,我们需要考虑两件事: ①:在业务方法执行之前对所有的输入参数进行格式处理——trim() ②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用

5.2 创建环境

创建一个 Maven 项目

pom.xml 添加 Spring 依赖

代码语言:javascript复制
<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 类

代码语言:javascript复制
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 的配置类

代码语言:javascript复制
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}

编写 App 运行类

代码语言:javascript复制
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。

5.3 具体实现

步骤 1:开启 SpringAOP 的注解功能
代码语言:javascript复制
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
步骤 2:编写通知类
代码语言:javascript复制
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}

}
步骤 3:添加环绕通知
代码语言:javascript复制
@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:完成核心业务,处理参数中的空格
代码语言:javascript复制
@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 类,在方法中将密码的长度进行打印

代码语言:javascript复制
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
        System.out.println(password.length());
        //模拟校验
        return password.equals("root");
    }
}

再次运行成功,就可以根据最终打印的长度来看看,字符串的空格有没有被去除掉。

注意:

0 人点赞