24

SpringBoot强化篇(八)-- Spring AOP

 3 years ago
source link: https://segmentfault.com/a/1190000038170973
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Spring AOP简介

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

bVbLz4L

AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。

AOP 应用场景分析?

实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现。

AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。

bVbLz5I

Spring AOP 应用原理分析

Spring AOP底层基于代理机制(动态方式)实现功能扩展:

  1. 假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
  2. 假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。

bVbLz6u

说明:Spring boot2.x 中AOP现在默认使用的CGLIB代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties)中进行如下配置:

#cglib aop proxy
#spring.aop.proxy-target-class=true
#jdk aop proxy
spring.aop.proxy-target-class=false

Spring 中AOP 相关术语分析

  • 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。
  • 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。
  • 连接点(joinpoint):程序执行过程中某个特定的点,一般指向被拦截到的目标方法。
  • 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。

bVbLz7p

Spring AOP快速实践

项目创建及配置

第一步创建maven项目或在已有项目基础上添加AOP启动依赖:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步定义业务层接口

package com.cy.pj.common.service;
public interface MailService {
    boolean sendMail(String msg);
}

第三步定义业务层实现类

@Service
public class MailServiceImpl implements MailService{
      @Override
     public boolean sendMail(String msg) {//ocp(开闭原则-->对扩展开放,对修改关闭)
         long t1=System.currentTimeMillis();
         System.out.println("send->"+msg);
         long t2=System.currentTimeMillis();
         System.out.println("send time:"+(t2-t1));
         return true; 
     }
}

我们自己计算了执行时间,但是违反了ocp原则,所以需要无侵入式扩展这个记录执行时间的功能。我们在这个类中添加内部类来实现两种方式的扩展。

package com.cy.pj.common.service;
import org.springframework.stereotype.Service;
@Service
public class MailServiceImpl implements MailService{
        @Override
         public boolean sendMail(String msg) {//ocp(开闭原则-->对扩展开放,对修改关闭)
            //long t1=System.currentTimeMillis();
             System.out.println("send->"+msg);
            //long t2=System.currentTimeMillis();
            //System.out.println("send time:"+(t2-t1));
             return true;
     }
}
//下面的两种设计了解?(基于原生方式实现功能扩展)
//自己动手写子类重写父类方法进行功能扩展
class TimeMailServiceImpl extends MailServiceImpl{//这种写法的原型就是CGLIB代理机制的方式(继承)
     @Override
     public boolean sendMail(String msg) {
         long t1=System.currentTimeMillis();
         boolean flag=super.sendMail(msg);
         long t2=System.currentTimeMillis();
         System.out.println("send time:"+(t2-t1));
         return flag;
     }
}
//自己写兄弟类对目标对象(兄弟类)进行功能扩展,这种方式又叫组合
class TimeMailServiceImpl2 implements MailService{//这种写法的原型就是JDK代理机制的方式(实现)
     private MailService mailService;
     public TimeMailServiceImpl2(MailService mailService){
            this.mailService=mailService;
     }
     @Override
     public boolean sendMail(String msg) {
         long t1=System.currentTimeMillis();
         boolean flag=mailService.sendMail(msg);
         long t2=System.currentTimeMillis();
         System.out.println("send time:"+(t2-t1));
         return flag;
     }
}

下来通过aop方式完成业务

package com.cy.pj.common.service.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//告诉spring我是一个切面(封装了扩展逻辑对象),这样的对象中要包含两部分内容(1.切入点,2.扩展逻辑-advice)
@Component//表示在spring中做一个注册
public class SysLogAspect {
     //定义切入点
     //bean表达式为spring中的一种粗粒度切入点表达式(不能精确到具体方法)
     //这里的mailServiceImpl名字为spring容器中一个bean对象的名字
     @Pointcut("bean(mailServiceImpl)")//多个bean的定义形式(bean(*ServiceImpl))
     public void doLogPointCut(){}//这个方法仅仅是承载切入点注解的一个载体,方法体内不需要写任何内容
     
     /*按照Aspect规范定义一个@Around环绕通知*/
     //@Around("bean(mailServiceImpl)")//直接在advice注解内部定义切入点表达式
     //对于@Around注解描述的方法器规范要求
     //1)返回值类型为Object(用于封装目标方法的执行结果)
     //2)参数类型为ProceedingJoinPoint(用于封装执行的目标方法信息)
     //3)抛出的异常Throwable(用于封装执行目标方法时抛出的异常)
     //4)在@Around注解描述的方法内部,可以手动调用目标方法
     @Around("doLogPointCut()")//也可以在advice注解内部通过方法引用引入切入点表达式
     public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
         long t1=System.currentTimeMillis();
         Object result = joinPoint.proceed();//表示调用目标方法
         long t2=System.currentTimeMillis();
         System.out.println("send time:"+(t2-t1));
         return result;
     }
}

编写测试类来测试实现

package com.cy.pj.common.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class MailServiceTests {
     @Autowired
     private MailService mailService;
     @Test//面向切面的测试类
     void testSendMail03(){
            mailService.sendMail("hello mailService");
     }
     @Test//自己动手写的子类测试
     void testSendMail01(){
    //new TimeMailServiceImpl().sendMail("hello aop");
     }
     @Test//自己动手写的兄弟类测试
     void testSendMail02(){
    //new TimeMailServiceImpl2(new MailServiceImpl()).sendMail("hello CGB2007");
     }
}

整个aop面向切面编程的过程图示

bVcKj0p

debug追踪cglib代理的注入对象

bVcKj0r

debug追踪jdk代理的注入对象

bVcKj0u

应用总结分析

代理过程图示分析

bVbLAcF

基于JDK代理方式实现

假如目标对象有实现接口,则可以基于JDK为目标对象创建代理对象,然后为目标对象进行功能扩展

bVbLAc3

说明:假如目标对象类型没有实现接口,则不允许使用JDK代理。

基于CGLIB代理方式实现

假如目标对象没有实现接口(当然实现了接口也是可以的),可以基于CGLIB代理方式为目标对象织入功能扩展

bVbLAgR

说明:目标对象实现了接口也可以基于CGLIB为目标对象创建代理对象。但是目标对象类型假如使用了final修饰,则不可以使用CGBLIB。

切面通知应用增强

通知类型

在基于Spring AOP编程的过程中,基于AspectJ框架标准,spring中定义了五种类型的通知(通知-Advice描述的是一种扩展业务),它们分别是:

  • @Before。(目标方法执行之前执行)
  • @AfterReturning。(目标方法成功结束时执行)
  • @AfterThrowing。(目标方法异常结束时执行)
  • @After。(目标方法结束时执行)
  • @Around.(重点掌握,目标方法执行前后都可以做业务拓展)(优先级最高)
    说明:在切面类中使用什么通知,由业务决定,并不是说,在切面中要把所有通知都写上。
package com.cy.pj.common.aspect;
@Component
@Aspect
public class SysTimeAspect {
    @Pointcut("bean(sysUserServiceImpl)")
    public void doTime(){}
 
    @Before("doTime()")
    public void doBefore(){
        System.out.println("time doBefore()");
    }
    @After("doTime()")
    public void doAfter(){
        System.out.println("time doAfter()");
    }
    /**核心业务正常结束时执行* 说明:假如有after,先执行after,再执行returning*/
    @AfterReturning("doTime()")
    public void doAfterReturning(){
        System.out.println("time doAfterReturning");
    }
    /**核心业务出现异常时执行说明:假如有after,先执行after,再执行Throwing*/
    @AfterThrowing("doTime()")
    public void doAfterThrowing(){
        System.out.println("time doAfterThrowing");
    }
    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint jp)
            throws Throwable{
        System.out.println("doAround.before");
         try{
         Object obj=jp.proceed();
           System.out.println("doAround.after");
          return obj;
         }catch(Throwable e){
          System.out.println(e.getMessage());
          throw e;
         }
        
    }
}

切入点表达式增强

bean表达式(重点)

bean表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:

  • bean("userServiceImpl")指定一个userServiceImpl类中所有方法。
  • bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法。

说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。

缺陷:不能精确到具体方法,也不能针对于具体模块包中的方法做切入点设计

within表达式

within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:

  • within("aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法。
  • within("aop.service.*") 指定当前目录下的所有类的所有方法。
  • within("aop.service..*") 指定当前目录以及子目录中类的所有方法。

within表达式应用场景分析:

1)对所有业务bean都要进行功能增强,但是bean名字又没有规则。

2)按业务模块(不同包下的业务)对bean对象进行业务功能增强。

execution表达式

execution表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:

语法:execution(返回值类型 包名.类名.方法名(参数列表))。

  • execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
  • execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。
  • execution( aop.service.. .*(..)) 万能配置。

@annotation表达式(重点)

@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析

  • @annotation(anno.RequiredLog) 匹配有此注解描述的方法。
  • @annotation(anno.RequiredCache) 匹配有此注解描述的方法。

切面优先级设置实现

切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。例如:

定义日志切面并指定优先级。

@Order(1)
@Aspect
@Component
public class SysLogAspect {
 …
}
定义缓存切面并指定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}

说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤器链、拦截器链

关键对象与术语总结

Spring 基于AspectJ框架实现AOP设计的关键对象概览

bVbLAsi

Spring AOP事务处理

Spring 中事务简介

事务定义

事务(Transaction)是一个业务,是一个不可分割的逻辑工作单元,基于事务可以更好的保证业务的正确性。

事务特性(ACID)

  • 原子性(Atomicity):一个事务中的多个操作要么都成功要么都失败。
  • 一致性(Consistency): 例如存钱操作,存之前和存之后的总钱数应该是一致的。
  • 隔离性(Isolation):事务与事务应该是相互隔离的。
  • 持久性(Durability):事务一旦提交,数据要持久保存。

说明:目前市场上在事务一致性方面,通常会做一定的优化,比方说只要最终一致就可以了,这样的事务我们通常会称之为柔性事务(只要最终一致就可以了).

Spring 中事务管理

Spring 中事务方式概述

Spring框架中提供了一种声明式事务的处理方式,此方式基于AOP代理,可以将具体业务逻辑与事务处理进行解耦。也就是让我们的业务代码逻辑不受污染或少量污染,就可以实现事务控制。

在SpringBoot项目中,其内部提供了事务的自动配置,当我们在项目中添加了指定依赖spring-boot-starter-jdbc时,框架会自动为我们的项目注入事务管理器对象,最常用的为DataSourceTransactionManager对象。

Spring 中事务管理实现

实际项目中最常用的注解方式的事务管理,以注解@Transactional配置方式为例,进行实践分析。

基于@Transactional 注解进行声明式事务管理的实现步骤分为两步:

1) 启用声明式事务管理,在项目启动类上添加@EnableTransactionManagement,新版本中也可不添加(例如新版Spring Boot项目)。

2) 将@Transactional注解添加到合适的业务类或方法上,并设置合适的属性信息。

@Transactional(timeout = 30,
 readOnly = false,
 isolation = Isolation.READ_COMMITTED,
 rollbackFor = Throwable.class,
 propagation = Propagation.REQUIRED)
 @Service
 public class implements SysUserService {
 @Transactional(readOnly = true)
 @Override
 public PageObject<SysUserDeptVo> findPageObjects(
 String username, Integer pageCurrent) {
 …
 }
}

其中,代码中的@Transactional注解用于描述类或方法,告诉spring框架我们要在此类的方法执行时进行事务控制,其具体说明如下:。

▪ 当@Transactional注解应用在类上时表示类中所有方法启动事务管理,并且一般用于事务共性的定义。

▪ 当@Transactional描述方法时表示此方法要进行事务管理,假如类和方法上都有@Transactional注解,则方法上的事务特性优先级比较高。

@Transactional常用属性应用说明:

▪ timeout:事务的超时时间,默认值为-1,表示没有超时显示。如果配置了具体时间,则超过该时间限制但事务还没有完成,则自动回滚事务。这个时间的记录方式是在事务开启以后到sql语句执行之前。

▪ read-only:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置read-only为true。对添加,修改,删除业务read-only的值应该为false。

▪ rollback-for:用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。

▪ no-rollback- for:抛出no-rollback-for 指定的异常类型,不回滚事务。

▪ isolation:事务的隔离级别,默认值采用 DEFAULT。当多个事务并发执行时,可能会出现脏读,不可重复读,幻读等现象时,但假如不希望出现这些现象可考虑修改事务的隔离级别(但隔离级别越高并发就会越小,性能就会越差)

Spring中事务控制过程分析

bVbLIcd

Spring事务管理是基于接口代理(JDK)或动态字节码(CGLIB)技术,然后通过AOP实施事务增强的。当我们执行添加了事务特性的目标方式时,系统会通过目标对象的代理对象调用DataSourceTransactionManager对象,在事务开始的时,执行doBegin方法,事务结束时执行doCommit或doRollback方法。

Spring 中事务传播特性

事务传播(Propagation)特性指"不同业务(service)对象"中的事务方法之间相互调用时,事务的传播方式

bVbLIcf

其中,常用事务传播方式:

@Transactional(propagation=Propagation.REQUIRED)。

如果没有事务创建新事务, 如果当前有事务参与当前事务, Spring 默认的事务传播行为是PROPAGATION_REQUIRED,它适合于绝大多数的情况。假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:

Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

bVbLIcp

@Transactional(propagation = Propagation.REQUIRED)
    @Override
    public List<Node> findZtreeMenuNodes() {
        return sysMenuDao.findZtreeMenuNodes();
    }

当有一个业务对象调用如上方法时,此方法始终工作在一个已经存在的事务方法,或者是由调用者创建的一个事务方法中。

@Transactional(propagation=Propagation.REQUIRES_NEW)。

必须是新事务, 如果有当前事务, 挂起当前事务并且开启新事务,

bVbLIcE

@Transactional(propagation = Propagation.REQUIRES_NEW)
 @Override
 public void saveObject(SysLog entity) {
    sysLogDao.insertObject(entity);
 }

当有一个业务对象调用如上业务方法时,此方法会始终运行在一个新的事务中。

Spring AOP 异步操作实现

异步场景分析

在开发系统的过程中,通常会考虑到系统的性能问题,提升系统性能的一个重要思想就是“串行”改“并行”。说起“并行”自然离不开“异步”,今天我们就来聊聊如何使用Spring的@Async的异步注解。

Spring 业务的异步实现

启动异步配置

在基于注解方式的配置中,借助@EnableAsync注解进行异步启动声明,Spring Boot版的项目中,将@EnableAsync注解应用到启动类上

@EnableAsync //spring容器启动时会创建线程池
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Spring中@Async注解应用

在需要异步执行的业务方法上,使用@Async方法进行异步声明。

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void saveObject(SysLog entity) {
     System.out.println("SysLogServiceImpl.save:"+
     Thread.currentThread().getName());
     sysLogDao.insertObject(entity);
     //try{Thread.sleep(5000);}catch(Exception e) {}
 }

假如需要获取业务层异步方法的执行结果,可参考如下代码设计进行实现:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    @Async
    @Override
    public Future<Integer> saveObject(SysLog entity) {
        System.out.println("SysLogServiceImpl.save:"+
        Thread.currentThread().getName());
        int rows=sysLogDao.insertObject(entity);
        //try{Thread.sleep(5000);}catch(Exception e) {}
        return new AsyncResult<Integer>(rows);
    }

其中,AsyncResult对象可以对异步方法的执行结果进行封装,假如外界需要异步方法结果时,可以通过Future对象的get方法获取结果。

当我们需要自己对spring框架提供的线程池进行一些简易配置

spring:
  task:
    execution:
      pool:
        queue-capacity: 128
        core-size: 5
        max-size: 128
        keep-alive: 60000
      thread-name-prefix: db-service-task-

对于spring框架中线程池配置参数的涵义,可以参考ThreadPoolExecutor对象中的解释。

说明:对于@Async注解默认会基于ThreadPoolTaskExecutor对象获取工作线程,然后调用由@Async描述的方法,让方法运行于一个工作线程,以实现异步操作。但是假如系统中的默认拒绝处理策略,任务执行过程的异常处理不能满足我们自身业务需求的话,我可以对异步线程池进行自定义.(SpringBoot中默认的异步配置可以参考自动配置对象TaskExecutionAutoConfiguration).

Spring 自定义异步池的实现(拓展)

为了让Spring中的异步池更好的服务于我们的业务,同时也尽量避免OOM,可以自定义线程池优化设计如下:

package com.cy.pj.common.config
@Slf4j
@Setter
@Configuration
@ConfigurationProperties("async-thread-pool")
public class SpringAsyncConfig implements AsyncConfigurer{
    /**核心线程数*/
    private int corePoolSize=20;
    /**最大线程数*/
    private int maximumPoolSize=1000;
    /**线程空闲时间*/
    private int keepAliveTime=30;
    /**阻塞队列容量*/
    private int queueCapacity=200;
    /**构建线程工厂*/
    private ThreadFactory threadFactory=new ThreadFactory() {
        //CAS算法
        private AtomicInteger at=new AtomicInteger(1000);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, 
"db-async-thread-"+at.getAndIncrement());
        }
    };    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maximumPoolSize);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setQueueCapacity(queueCapacity);
        executor.setRejectedExecutionHandler((Runnable r, 
 ThreadPoolExecutor exe) -> {
                log.warn("当前任务线程池队列已满.");
        });
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler 
getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler() {
            @Override
            public void handleUncaughtException(Throwable ex ,
 Method method , Object... params) {
                log.error("线程池执行任务发生未知异常.", ex);
            }
        };
    }}

其中:@ConfigurationProperties("async-thread-pool")的含义是读取application.yml配置文件中以"async-thread-pool"名为前缀的配置信息,并通过所描述类的set方法赋值给对应的属性,在application.yml中连接器池的关键配置如下:

async-thread-pool:
       corePoolSize: 20
       maxPoolSize: 1000
       keepAliveSeconds: 30
       queueCapacity: 1000

后续在业务类中,假如我们使用@Async注解描述业务方法,默认会使用ThreadPoolTaskExecutor池对象中的线程执行异步任务。

Spring AOP中Cache操作实现

缓存场景分析

在业务方法中我们可能调用数据层方法获取数据库中数据,假如访问数据的频率比较高,为了提高的查询效率,降低数据库的访问压力,可以在业务层对数据进行缓存.

Spring 中业务缓存应用实现过程

启动缓存配置

在项目(SpringBoot项目)的启动类上添加@EnableCaching注解,以启动缓存配置。

package com.cy;
/**
* 异步的自动配置生效).
 * @EnableCaching 注解表示启动缓存配置
 */
@EnableCaching
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

业务方法上应用缓存配置

在需要进行缓存的业务方法上通过@Cacheable注解对方法进行相关描述.表示方法的返回值要存储到Cache中,假如在更新操作时需要将cache中的数据移除,可以在更新方法上使用@CacheEvict注解对方法进行描述。

第一步:在相关模块查询相关业务方法中,使用缓存

@Cacheable(value = "menuCache")
@Transactional(readOnly = true)
public List<Map<String,Object>> findObjects() {
....
}

其中,value属性的值表示要使用的缓存对象,名字自己指定,其中底层为一个map对象,当向cache中添加数据时,key默认为方法实际参数的组合。

第二步:在相关模块更新时,清除指定缓存数据

allEntries表示清除所有。

@CacheEvict(value="menuCache",allEntries=true)
 @Override
 public int saveObject(SysDept entity) {...}

spring中的缓存应用原理

bVbLIQy

进行一个小案例

第一步定义业务接口

package com.cy.pj.module.service;
import java.util.List;
public interface ModuleService {
    List<String> findPermissions();
}

第二步定义业务实现类

package com.cy.pj.module.service;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ModuleServiceImpl implements ModuleService{
    @Override
     public List<String> findPermissions() {
         System.out.println("select permissions from database");
         List<String> list=new ArrayList<>();
         list.add("sys:log:delete");
         list.add("sys:log:select");//假设这些数据来自数据库
         return list;
     }
}

第三步定义切面

package com.cy.pj.commom.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Aspect
public class CacheAspect {//系统底层会将这个切面中的内容转换为Advisor对象

    //假设这个map就是我们的一个小cache,我们从数据库取出的数据可以存储到此cache中
    private Map<String,Object> cache=new ConcurrentHashMap<>();

    @Pointcut("bean(moduleServiceImpl)")
    public void doCache(){}

    @Around("doCache()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        //1.从cache中取数据
        Object obj = cache.get("userPer");//假设key为userPer
        if(obj!=null) return obj;
        //2.cache中没有则查数据
        obj= joinPoint.proceed();
        //3.将数据存储到cache
        cache.put("userPer", obj);
        return obj;
    }
}

第四步编写测试类测试

package com.cy.pj.module.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class ModuleServiceTests {

    @Autowired
    private ModuleService moduleService;

    @Test
    void testFindPermissions(){
        List<String> permissions = moduleService.findPermissions();
        permissions = moduleService.findPermissions();
        permissions = moduleService.findPermissions();
    }
}

Spring AOP原生方式实现

Spring 整合AspectJ框架实现AOP只是Spring框架中AOP的一种实现方式,此方式相对比较简单,实现方便。但此方式底层还是要转换为Spring原生AOP的实现,Spring AOP原生方式实现的核心有两大部分构成,分别是:

▪ 代理(JDK,CGLIB)。

▪ org.aopalliance包下的拦截体系。

以Spring中一种原生AOP架构的基本实现为例进行原理分析和说明,其简易架构

bVbLIQ1

其中DefaultAdvisorAutoProxyCreator这个类功能更为强大,这个类的奇妙之处是他实现BeanPostProcessor接口,当ApplicationContext读取所有的Bean配置信息后,这个类将扫描上下文,寻找所有的Advisor对象(一个Advisor由切入点和通知组成),将这些Advisor应用到所有符合切入点的Bean中。

核心业务接口定义及实现

定义邮件业务接口,用于定义搜索业务规范

package com.cy.pj.common.service;
public interface MailService {
    boolean sendMsg(String message);
}

定义邮件业务接口实现

package com.cy.pj.common.service;

import org.springframework.stereotype.Service;

@Service
public class MailServiceImpl implements MailService{

    @Override
    public boolean sendMsg(String message) {
        System.out.println("send->"+message);
        return true;
    }

}

定义LogAdvice对象,基于此对象为目标业务对象做日志增强。

其中,MethodInterceptor对象继承Advice对象,基于此对象方法可以对目标方法进行拦截。

package com.cy.pj.common.advisor;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**封装了扩展业务逻辑的对象,这样的对象在原生的aop中需要在advisor中注册*/
public class LogAdvice implements MethodInterceptor {//Advice

    /**此方法可以在目标业务方法执行之前和之后添加扩展逻辑*/
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("start:"+System.nanoTime());
        Object result = methodInvocation.proceed();//执行目标方法
        System.out.println("end:"+System.nanoTime());
        return result;
    }
}

日志Advisor对象定义及实现

其中,StaticMethodMatcherPointcutAdvisor类为Spring框架中定义的一种Advisor,我们自己写的Advisor可以直接继承此类进行资源整合。

package com.cy.pj.common.advisor;

import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 此Advisor中定义了一种规范
 * 1)定义了哪些方法为切入点方法
 * 2)定义了在切入点方法执行时要植入的通知(扩展逻辑)
 */
@Component
public class LogAdvisor extends StaticMethodMatcherPointcutAdvisor {
    //定通知 advice
    public LogAdvisor(){
        setAdvice(new LogAdvice());
    }

    //定切点 pointcut
    /**
     * matches方法中可以获取我们要执行的目标方法,并且我们可以在此判断这个目标方法是否为我们的一个切入点方法
     * 1)返回值为true表示目标方法为切入点方法(在此方法执行时可以植入扩展逻辑)
     * 2)返回值为false表示目标方法为非切入点方法
     */
    @Override
    public boolean matches(Method method, Class<?> aClass) {
        try {
            Method targetMethod =
                    aClass.getMethod(method.getName(), method.getParameterTypes());
            return targetMethod.getName().equals("sendMsg");
        }catch (Exception e){
            return false;
        }
    }
}

BeanPostProcessor类型对象初始化

在项目启动类中,添加DefaultAdvisorAutoProxyCreator对象初始化方法,基于此对象在容器启动时扫描所有Advisor对象,然后基于切入点描述的目标方法为目标对象创建代理对象

package com.cy;

import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

    @Bean //@Bean注解描述方法时,这个方法的返回值交给spring管理
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        return new DefaultAdvisorAutoProxyCreator();
    }
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

基于Spring boot项目进行单元测试

package com.cy.pj.common.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MailServiceTests {
    @Autowired
    private MailService mailService;

    @Test
    void testSendMsg(){
        System.out.println(mailService.sendMsg("hello spring aop"));
    }
}

原生实现AOP的过程分析图

bVcHSwE


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK