AOP——面向切面编程

一、AOP概念

1.AOP概念

在Spring中,有两个核心的概念,一个是IOC/DI,一个是AOP。那么什么是AOP?

AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。

OOP(Object Oriented Programming)面向对象编程,是一种编程思想,所以,AOP也是一种编程思想,只是他们两个是不同的编程范式。

2.AOP作用

在不惊动原始设计的基础上为其进行功能增强。

3.AOP核心概念

  • 连接点(JoinPoint):程序执行过程中都任意位置,粒度为执行方法、抛出异常、设置变量等。

在SpringAOP中,理解为方法的执行

  • 切入点(Pointcut):匹配连接点的式子

在SpringAOP中,一个切入点可以描述一个具体方法,也可以匹配多个方法

 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点

  •  通知(Advice):在切入点处执行的操作,也就是共性功能

在SpringAOP中,功能最终以方法的形式呈现

  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系

换位理解

  • 连接点(可选择的所有英雄)
  • 切入点(已选择的英雄)
  • 通知(某个增益buff)
  • 通知类(所有增益buff)
  • 切面(带了buff的已选择英雄)

二、AOP入门 

1.导入坐标

新建一个Maven项目,在pom.xml中添加依赖.

    
        
            org.springframework
            spring-context
            5.3.9
        
        
            org.aspectj
            aspectjweaver
            1.9.8
        
    
  • 在spring-context包中已经导入了spring-aop,所以不需要在单独导入spring-aop
  • 导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以采用Spring整合AspectJ的方式进行AOP开发

2.定义接口和实现类

在java包下添加BookDao和BookDaoImpl类(连接点)

BookDao

public interface BookDao {
    public void save();
    public void update();
}

BookDaoImpl

@Repository
public class BookDaoImpl implements BookDao {

    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }

    @Override
    public void update() {
        System.out.println("book dao update ...");
    }
}

添加Spring配置类

SpringConfig

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

添加测试类

public class test {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

        BookDao bookDao = ctx.getBean(BookDao.class);

        bookDao.save();
        bookDao.update();
    }
}

项目结构如图

AOP——面向切面编程_第1张图片

 3.定义通知类和通知

通知时将共性功能抽取出来后形成的方法,这里的共性功能既是打印当前系统时间

在com包下创建aop.MyAdvice类

public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

类名方法名任意

4.定义切入点 

public class MyAdvice {
    //定义切入点update()
    @Pointcut("execution(void com.dao.BookDao.update())")
    private void pt(){}

    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑

5.制作切面

 切面是用来描述通知和切入点之间的关系

public class MyAdvice {
    //定义切入点update()
    @Pointcut("execution(void com.dao.BookDao.update())")
    private void pt(){}

    //制作切面
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

6.将通知类配给容器并标识其为切面类

@Component
@Aspect
public class MyAdvice {
    //定义切入点update()
    @Pointcut("execution(void com.dao.BookDao.update())")
    private void pt(){}

    //制作切面
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

7.开启注解格式AOP功能

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

8.运行测试

运行test测试类,控制台输出

AOP——面向切面编程_第2张图片

三、AOP工作流程

AOP是基于Spring容器管理的bean做增强,SpringAOP的本质是通过代理模式实现的。

1.Spring容器启动

  • 容器启动就需要去加载bean,在此之前需加载所需的类
  • 需要被增强的类,如:BookServiceImpl
  • 通知类:如:MyAdvice
  • 注意此时bean对象还没有创建成功

2.读取所有切面配置中的切入点

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dao.BookDao.save())")
    private void ptx(){}
    
    @Pointcut("execution(void com.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

3.初始化bean

判定bean对应的类中的方法是否匹配到任意切入点

  • 匹配失败,创建原始对象

匹配失败说明不需要增强,直接调用原始对象的方法即可

  • 匹配成功,创建原始对象(目标对象)的代理对象
  • 匹配成功说明需要对其进行增强
  • 对那个类做增强,这个类对应的对象就叫做目标对象
  • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
  • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

4.获取bean执行方法

  • 获取的bean是原始对象时,调用方法并执行,完成操作
  • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知(如:MyAdvice中的method方法)内容加进去,就实现了增强,这就是我们所说的代理(Proxy) 

四、AOP配置管理

1.AOP切入点表达式

@Pointcut("execution(void com.dao.BookDao.update())")

其中"execution(void com.dao.BookDao.update())"即是切入点表达式

1.1、语法格式

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

对于切入点的描述,有两种描述方式

一、执行com.dao包下BookDao接口中无参数update方法

execution(void com.dao.BookDao.update())

二、执行com.dao.impl包下BookDaoImpl类中无参数的update方法

execution(void com.dao.impl.BookDaoImpl.update())

切入点表达式标准格式:

动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

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

1.2、通配符 

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.*.UserService.find*(*))

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

  • ..: 多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))

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

  • +: 专用于匹配子类类型
 execution(* *..*Service+.*(..))

*Service+,表示所有以Service结尾的接口的子类(用的很少)

  •  以下是一些常见的通配符用法
execution(void com.dao.BookDao.update())

匹配接口,能匹配到

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

返回值任意,能匹配到

execution(* com.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.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

1.3、书写技巧 

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

2.AOP通知类型

2.1、类型介绍

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置

对于通知具体要添加到切入点的哪里,共提供了五种通知类型

  • 前置通知
  • 后置通知
  • 环绕通知
  • 返回后通知
  • 抛出异常后通知

以try/catch来看这几种通知类型

    public int methodName(){
        //代码1(前置通知添加的位置)
        try {
            //代码2(前置通知添加的位置)
            //原始的业务操作(我们要增强的方法)
            //代码3(返回后通知添加的位置)
        }catch (Exception e){
            //代码4(抛出异常后通知添加的位置)
        }
        //代码5(后置通知添加的位置)
    }
  • 前置通知:追加功能到方法执行前,类似于在代码1或者代码2添加内容
  • 后置通知:追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
  • 返回后通知:追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
  • 抛出异常后通知:追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
  • 环绕通知:环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能

2.2、环境准备 

新建一个Maven项目,在pom.xml中添加Spring依赖

    
        
            org.springframework
            spring-context
            5.3.9
        
        
            org.aspectj
            aspectjweaver
            1.9.8
        
    

创建BookDaoBookDaoImpl

BookDao

public interface BookDao {
    public void update();
    public int select();
}

 BookDaoImpl

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void update() {
        System.out.println("book dao update ...");
    }

    @Override
    public int select() {
        System.out.println("book dao select is running ...");
        return 100;
    }
}

创建Spring的配置类

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

创建通知类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dao.BookDao.update())")
    private void pt(){}

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

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

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

    public Object around() throws Throwable {
        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 ...");
    }
}

 编写测试类

public class test {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

2.3、@Before

前置通知,在MyAdvice中的before方法上添加@Before注解

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

也可写成@Before("MyAdvice.pt()")

控制台输出

AOP——面向切面编程_第3张图片

2.4、@After

后置通知,在MyAdvice中的after方法上添加@After注解

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

控制台输出

AOP——面向切面编程_第4张图片

2.5、@Around

环绕通知,在MyAdvice中的around方法上添加@Around注解

    @Around("pt()")//一个错误示例
    public void around() {
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }

控制台输出

已将上文@After和@Before注释

AOP——面向切面编程_第5张图片

此时通知内容被打印出来,但是原始方法的内容却没有被执行

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,可依赖形参ProceedingJoinPoint实现对原始方法的调用

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

因为无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

控制台输出 

AOP——面向切面编程_第6张图片

对原始方法有返回值的处理

将around上的pt()改为pt2()

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

 在test中添加对应对象调用

public class test {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();

        int num = bookDao.select();
        System.out.println(num);
    }
}

控制台输出

AOP——面向切面编程_第7张图片

此时报错Null return value from advice does not match primitive return type for: public abstract int com.dao.BookDao.select()说明空的返回不匹配原始方法的int返回,所以当我们使用环绕通知,需根据原始方法的返回值来设置环绕通知的返回值

    @Around("pt2()")
    public Object around(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类型更通用 

此时控制台输出

AOP——面向切面编程_第8张图片

2.6、@AfterReturning

返回后通知,在MyAdvice中的afterReturning方法上添加@AfterReturning注解

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

控制台输出

已将上文@Around注释

AOP——面向切面编程_第9张图片

当select方法在执行时出现异常时

    public int select() {
        System.out.println("book dao select is running ...");
        //模拟异常
        int i = 1/0;
        return 100;
    }

此时运行test,控制台输出为

AOP——面向切面编程_第10张图片

返回后通知则不被执行 

2.7、@AfterThrowing

异常后通知,在MyAdvice中的afterThrowing方法上添加@AfterThrowing注解

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

控制台输出

AOP——面向切面编程_第11张图片

同样如果执行过程中没有抛异常,异常后通知将不会执行 

2.8、总结

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

@Component
@Aspect
public class MyAdvice1 {
    public Object around(ProceedingJoinPoint pjp){
        //增强代码1(前置通知)
        Object ret = null;
        try {
            //增强代码2(前置通知)
            ret=pjp.proceed();
            //增强代码3(返回后通知)
        }catch (Throwable t){
            //增强代码4(异常后通知)
        }
        //增强代码5(后置通知)
        return ret;
    }
}

环绕通知注意事项

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

3. 业务层接口执行效率

案例:实现任意业务层接口执行均可显示其执行效率(执行时常)

3.1、环境准备

以下环境准备即为Spring整合Mybatis、Junit中整合而成

数据库名: mybatis_db           表名:user

AOP——面向切面编程_第12张图片

创建一个Maven项目,在pom.xml中添加Spring依赖

    
        
            org.springframework
            spring-context
            5.3.9
        
        
            org.springframework
            spring-jdbc
            5.3.9
        
        
            org.springframework
            spring-test
            5.3.9
        
        
            org.aspectj
            aspectjweaver
            1.9.8
        
        
            com.alibaba
            druid
            1.1.16
        
        
            org.mybatis
            mybatis
            3.5.6
        
        
            org.mybatis
            mybatis-spring
            1.3.0
        
        
            mysql
            mysql-connector-java
            8.0.28
        
        
            junit
            junit
            4.13
            test
        
    

添加AccountServiceAccountServiceImplAccountDaoAccount

Account

public class Account implements Serializable {
    private Integer id;
    private String name;
    private String password;
    private Integer grade;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", grade=" + grade +
                '}';
    }
}

AccountDao

public interface AccountDao {
    @Insert("insert into user(name,password,grade) values (#{name},#{password},#{grade})")
    void save(Account account);

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

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

    @Select("select * from user")
    List findAll();

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

AccountService

public interface AccountService {
    void save(Account account);
    void delete(Integer id);
    void update(Account account);
    List findAll();
    Account findById(Integer id);
}

AccountServiceImpl

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

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

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

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

    @Override
    public List findAll() {
        return accountDao.findAll();
    }

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

在resources下创建一个jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis_db?useSSL=false
jdbc.username=root
jdbc.password=1234

创建相关配置类SpringConfig、JdbcConfig、MybatisConfig类

SpringConfig

@Configuration
@ComponentScan("com")
@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 sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.dao");
        return msc;
    }
}

编写Spring整合Junit的测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
    @Autowired
    private AccountService accountService;

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

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

最终项目结构如下

AOP——面向切面编程_第13张图片

3.2、开启SpringAOP的注解功能并创建通知类

在SpringConfig中添加@EnableAspectJAutoProxy注解

@Configuration
@ComponentScan("com")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy
public class SpringConfig {
}

创建com.aop.ProjectAdvice通知类

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层所有方法
    @Pointcut("execution(* com.service.*Service.*(..))")
    private void servicePt(){}

    public void runSpeed(){

    }
}

3.3、添加环绕通知及核心业务

在ProjectAdvice类中添加如下代码

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层所有方法
    @Pointcut("execution(* com.service.*Service.*(..))")
    private void servicePt(){}

    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //获取执行的签名信息
        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");
    }
}

3.4、运行单元测试类

控制台输出

4.AOP通知获取数据

AOP通知获取数据可以从获取参数、获取返回值和获取异常三个方面来研究

4.1、环境准备

创建一个Maven项目,在pom.xml中添加Spring依赖

    
        
            org.springframework
            spring-context
            5.3.9
        
        
            org.aspectj
            aspectjweaver
            1.9.8
        
    

添加BookDaoBookDaoImpl

BookDao

public interface BookDao {
    public String findName(int id, String password);
}
BookDaoImpl
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public String findName(int id, String password) {
        System.out.println("id:" + id);
        System.out.println("password:" + password);

        return "itcast";
    }
}

创建Spring的配置类

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

编写通知类

@Component
@Aspect
public class sss {
    @Pointcut("execution(* com.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(ProceedingJoinPoint pjp) 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 ...");
    }
}

编写test测试类

public class test {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100,"wanger");
        System.out.println(name);
    }
}

最终结构如下

AOP——面向切面编程_第14张图片

4.2、获取参数

  • 非环绕通知获取参数方式

非环绕通知通过在方法上添加JointPoint,通过JointPoint来获取参数

以before方法为例,其余非环绕方法同样

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

控制台输出

其余注解已被注释,下文同样

AOP——面向切面编程_第15张图片

 因为参数个数不确定,所以获取到的是一个数组形式参数

  • 环绕式通知获取参数方式

环绕式通知则使用ProceedingJoinPoint类,因为ProceedingJoinPointJoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法

    @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;
    }

控制台输出

AOP——面向切面编程_第16张图片

pjp.proceed()方法是有两个构造方法,分别是proceed()和proceed(Object[ ] objects)

  • 调用无参数的proceed(),当原始方法有参数,会在调用过程中自动传入参数
  • 所以调用这两个方法的任意一个都可以完成功能
  • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法
    @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;
    }

控制台输出

AOP——面向切面编程_第17张图片

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

4.3、获取返回值

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

  • 返回后通知获取返回值
    @AfterReturning(value = "pt()", returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..." + ret);
    }
  • 如果获取返回值同时也要获取参数,对于JoinPoint参数,如果有,必须放在第一位
  • 参数名必须一一对应
  • 参数类型也可以写成String类型,写成Object是为了方便匹配

控制台输出

AOP——面向切面编程_第18张图片

  • 环绕通知获取返回值 
    @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.4、获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕这两个通知类型可以获取

  • 环绕通知获取异常
    @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) {
            System.out.println(throwable);
            throwable.printStackTrace();
        }
        return ret;
    }

只需将异常捕获即可

在BookDaoImpl中模拟异常

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public String findName(int id, String password) {
        System.out.println("id:" + id);
        System.out.println("password:" + password);

        int i = 1 / 0;

        return "itcast";
    }
}

运行测试类,控制台输出

AOP——面向切面编程_第19张图片

  • 抛出异常后通知获取异常
    @AfterThrowing(value = "pt()", throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..." + t);
    }

同样注意参数一一对应及有JoinPoint时将JoinPoint放在第一位

控制台输出

AOP——面向切面编程_第20张图片

5.模拟百度网盘密码进行数据兼容处理

AOP——面向切面编程_第21张图片

在我们使用百度网盘分享连接时,常常会复制提取码,但有时会多复制到一些空格,直接粘贴到百度网盘的提取输入框,但提取码对应密钥是没有空格的,此时如果不做处理,直接对比的话,就会导致提取码不一致,无法访问百度网盘上的内容,此时便需要我们对所输入参数进行格式处理——trim()

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

创建一个Maven项目,在pom.xml中添加Spring依赖

    
        
            org.springframework
            spring-context
            5.3.9
        
        
            org.aspectj
            aspectjweaver
            1.9.8
        
    

添加ResourcesServiceResourcesServiceImpl,ResourcesDaoResourcesDaoImpl类 

ResourcesDao

public interface ResourceDao {
    boolean readResources(String url,String password);
}

ResourcesDaoImpl

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

ResourcesService

public interface ResourceService {
    public boolean openURL(String url,String password);
}

ResourcesServiceImpl

@Service
public class ResourcesServiceImpl implements ResourceService {
    @Autowired
    private ResourceDao resourceDao;

    @Override
    public boolean openURL(String url, String password) {
        return resourceDao.readResources(url, password);
    }
}

创建Spring的配置类

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

编写通知类及添加环绕通知完成核心业务

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.service.*Service.*(..))")
    private void 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;
    }
}

编写test测试类

public class test {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        ResourceService resourceService = ctx.getBean(ResourceService.class);
        boolean flag = resourceService.openURL("http://pan.baidu.com/hhh","root ");
        System.out.println(flag);
    }
}

控制台输出

AOP——面向切面编程_第22张图片

你可能感兴趣的:(Spring学习笔记,spring,java,后端)