SpringBoot工程中AOP应用
in JavaDevelop with 0 comment

SpringBoot工程中AOP应用

in JavaDevelop with 0 comment

AOP 简介

背景分析

对于一个业务而言,我们如何在不修改源代码的基础上对对象功能进行拓展,例如现有一个公告(通知)业务接口及实现:

pubic interface NoticeService{
    int deleteById(Integer…ids);
}
123
public class NoticeServiceImpl implements NoticeService{
   public int deleteById(Integer…ids){
       System.out.println(Arrays.toString(ids));
       return 0 ;
   }
}

需求:基于OCP(开闭原则-对扩展开放对修改关闭)设计原则对NoticeServiceImpl类的功能进行扩展,例如在deleteById业务方法执行之前和之后输出一下系统时间.

方案1:基于继承方式实现其功能扩展,关键设计如下:

public class CglibLogNoticeService extends NoticeServiceImpl{
   public int deleteById(Integer…ids){
       System.out.println("Start:"+System.currentTimeMillis());
       Int rows=super.deleteById(ids);
       System.out.println("After:"+System.currentTimeMillis());
       return rows;
    }
}

测试类如下:

public class NoticeServiceTests{
     public static void main(String[] args){
         NoticeService ns=new CglibLogNoticeService();
         ns.deleteById(10,20,30);
     }
}

其中,基于继承方式实现功能扩展,代码简单,容器理解,但是不够灵活,耦合性比较强。

方案2:基于组合方式实现其功能扩展,关键代码设计如下:

public class JdkLogNoticeService implements NoticeService{
      private NoticeService noticeService;//has a 
      public  JdkLogNoticeService(NoticeService noticeService){
        this.noticeService=noticeService;
      }
     public int deleteById(Integer…ids){
         System.out.println("Start:"+System.currentTimeMillis());
         int rows=this.noticeService.deleteById(ids);
         System.out.println("After:"+System.currentTimeMillis());
         return rows;
      }
}

测试类

public class NoticeServiceTests{
     public static void main(String[] args){
         NoticeService ns=
         new JdkLogNoticeService(new NoticeServiceImpl());
         ns.deleteById(10,20);
     }
}

其中,基于组合方式实现功能扩展,代码比较灵活,耦合低,稳定性强,但理解相对比较困难。

总之,无论是继承,还是组合都是基于OCP方式实现了对象功能扩展,都有相应的优缺点,并且我们都要自己去写这些子类或兄弟类,在这些类中调用目标对象(父类或兄弟类对象)的方法以及扩展业务逻辑.对于这样的模板代码我们能否进行简化呢?例如.由框架实现其共性(创建目录类型的子类类型或兄弟类型),特性交给用户自己实现.

AOP概述

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)。如图所示:
在这里插入图片描述
AOP 与 OOP 字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。

实现原理

AOP可以在系统启动时为目标类型创建子类或兄弟类型对象,这样的对象我们通常会称之为动态代理对象.如图所示:

在这里插入图片描述

其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种(先了解):

相关术语分析

说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是 通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。

Spring AOP 快速入门

业务描述

在项目中定义一个日志切面,通过切面中的通知方法为目标业务对象做日志功能增强。

添加AOP依赖

项目中添加aop应用依赖,代码如下:

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

说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵守 java 规范的 class 文件。

业务切面对象设计

通过设计切面对象,为目标业务方法做功能增强,关键步骤如下:
第一步:创建注解类型,应用于切入点表达式的定义,关键代码如下:

package com.cy.pj.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
       String operation();
}

第二步:创建切面对象,用于做日志业务增强,关键代码如下:

package com.cy.pj.sys.service.aspect;

import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.sys.pojo.SysLog;
import com.fasterxml.jackson.databind.ObjectMapper;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class SysLogAspect {

    private static final Logger log= LoggerFactory.getLogger(SysLogAspect.class);
    /**
     * @Pointcut注解用于定义切入点
     * @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
     * 点方法
     */
    @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
    public void doLog(){}//此方法只负责承载切入点的定义

    /**
     * @Around注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
     * @param jp 连接点对象,此对象封装了要执行的目标方法信息.
     * 可以通过连接点对象调用目标方法.
     * @return 目标方法的执行结果
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint jp)throws Throwable{
        long t1=System.currentTimeMillis();
        try {
            //执行目标方法(切点方法中的某个方法)
            Object result = jp.proceed();
            long t2=System.currentTimeMillis();
            log.info("opertime:{}",t2-t1); return result;//目标业务方法的执行结果
        }catch(Throwable e){
            e.printStackTrace();
            long t2=System.currentTimeMillis();
            log.info("exception:{}",e.getMessage());
            throw e;
        }
    }

第三步:通过注解RequiredLog注解描述日志查询或删除业务相关方法,此时这个方法为日志切入点方法,例如:

@RequiredLog(operation="公告查询")
@Override
public List<SysLog> findLogs(SysLog sysLog) {
List<SysLog> list=syslogDao.selectLogs(sysLog);
    return list;
}

第四步:测试通知业务方法,并检测日志输出以及了解其运行原理,如图所示:

在这里插入图片描述

获取并记录详细日志

第一步:定义日志pojo对象,用于封装日志信息,例如:

package com.pj.sys.pojo;

import java.util.Date;

public class SysLog {
    private Integer id;
    private String ip;
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private Integer status;
    private String error;
    private Date createdTime;
    //自己添加set/get/toString等方法
}

第二步:修改日切面对象,获取并记录详细日志,关键代码如下:

package com.pj.sys.service.aspect;

import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.sys.pojo.SysLog;
import com.fasterxml.jackson.databind.ObjectMapper;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class SysLogAspect {

    
    private static final Logger log=LoggerFactory.getLogger(SysLogAspect.class);
    /**
     * @Pointcut注解用于定义切入点
     * @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
     * 点方法
     */
    @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
    public void doLog(){}//此方法只负责承载切入点的定义

    /**
     * @Around注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
     * @param jp 连接点对象,此对象封装了要执行的目标方法信息.
     * 可以通过连接点对象调用目标方法.
     * @return 目标方法的执行结果
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint jp)throws Throwable{
        long t1=System.currentTimeMillis();
        log.info("Start:{}",t1);
        try {
            //执行目标方法(切点方法中的某个方法)
            Object result = jp.proceed();
            long t2=System.currentTimeMillis();
            log.info("After:{}",t2);
            doLogInfo(jp,t2-t1,null);
            return result;//目标业务方法的执行结果
        }catch(Throwable e){
            e.printStackTrace();
            long t2=System.currentTimeMillis();
            doLogInfo(jp,t2-t1,e);
            throw e;
        }
    }

    //记录用户行为日志
    private void doLogInfo(ProceedingJoinPoint jp,long time,Throwable e) throws Exception {
        //1.获取用户行为日志
        //1.1获取登录用户名(没做登录时,可以先给个固定值)
        String username="cgb";
        //1.2获取ip地址
        String ip= "202.106.0.20";
        //1.3获取操作名(operation)-@RequiredLog注解中value属性的值
        //1.3.1获取目标对象类型
        Class<?> targetCls=jp.getTarget().getClass();
        //1.3.2获取目标方法
        MethodSignature ms=(MethodSignature) jp.getSignature();//方法签名
        Method targetMethod=targetCls.getMethod(ms.getName(),ms.getParameterTypes());
        //1.3.3 获取方法上RequiredLog注解
        RequiredLog annotation =targetMethod.getAnnotation(RequiredLog.class);
        //1.3.4 获取注解中定义操作名
        String operation=annotation.operation();
        //1.4获取方法声明(类全名+方法名)
        String classMethodName=targetCls.getName()+"."+targetMethod.getName();
        //1.5获取方法实际参数信息
        Object[]args=jp.getArgs();
        String params=new ObjectMapper().writeValueAsString(args);
        //2.封装用户行为日志
        SysLog sysLog=new SysLog();
        sysLog.setUsername(username);
        sysLog.setIp(ip);
        sysLog.setOperation(operation);
        sysLog.setMethod(classMethodName);
        sysLog.setParams(params);
        sysLog.setTime(time);
        if(e!=null) {
            sysLog.setStatus(0);
            sysLog.setError(e.getMessage());
        }
        //3.打印日志
        String userLog=new ObjectMapper().writeValueAsString(sysLog);
        log.info("user.oper {}",userLog);
    }

}

第三步:进行日志业务查询,并检测是否有详细日志输出。

Spring AOP 技术进阶

通知类型

Spring框架AOP模块定义通知类型,有如下几种:

案例分析如下:(了解,可选择进行实现)

第一步:定义注解,代码如下:

package com.cy.pj.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredTime{}

第二步:定义时间切面对象对象演示通知执行,关键代码如下:

package com.cy.pj.sys.service.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SysTimeAspect {
       
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredTime)")
   public void doTime(){}

    @Before("doTime()")
    public void doBefore(){
        System.out.println("@Before");
    }

    @After("doTime()")
    public void doAfter(){
        System.out.println("@After");
    }
    @AfterReturning("doTime()")
    public void doAfterReturning(){
        System.out.println("@AfterReturning");
    }

    @AfterThrowing("doTime()")
    public void doAfterThrowing(){
        System.out.println("@AfterThrowing");
    }
    //最重要,优先级也是最高
    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
        try {
            System.out.println("@Around.before");
            Object result = joinPoint.proceed();
            System.out.println("@Around.AfterReturning");
            return result;
        }catch(Exception e){
            System.out.println("@Around.AfterThrowing");
            e.printStackTrace();
            throw e;
        }finally {
            System.out.println("@Around.after");
        }
    }
}

结合业务方法进行测试,检查通知方法执行顺序

切面执行顺序

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

@Order(1)
@Aspect
@Component
public class SysLogAspect {
 …
}

定义缓存切面并指定优先级:

@Order(2)
@Aspect
@Component
public class SysCacheAspect {
	…
}

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

使用 aop 进行事务控制

项目文件结构

图片描述

aop 的应用场景之一就是事务控制,下面学习使用 aop 进行事务控制。

修改SpringbootAop.java

package com.shiyanlou.springboot;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;

@Aspect
@Component
public class SpringbootAop {


    //设置切点
    @Pointcut(value = "execution(* com.shiyanlou.springboot..*.run(..))")
    public void aop() {
    }

    @Before("aop()")
    public void before() {
        System.out.println("before:执行方法前");
    }


    @After("aop()")
    public void after() {
        System.out.println("after:执行方法后");
    }


    @AfterThrowing("aop()")
    public void afterThrowing() {
        System.out.println("afterThrowing:异常抛出后");
    }

    @AfterReturning("aop()")
    public void afterReturning() {
        System.out.println("afterReturning:方法返回后");
    }

    @Around("aop()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("around:环绕通知前");
        //执行方法
        joinPoint.proceed();
        System.out.println("around:环绕通知后");

    }


    /**
     * 注入事务管理器
     */
    @Autowired
    public PlatformTransactionManager platformTransactionManager;

    /**
     * 设置事务拦截器
     */
    @Bean
    public TransactionInterceptor transactionInterceptor() {
        //设置事务属性 可以通过它设置事务的基本属性,如事务是读写事务或者只读事务,事务的超时时间等
        DefaultTransactionAttribute defaultTransactionAttribute = new DefaultTransactionAttribute();
        //设置为读写事务
        defaultTransactionAttribute.setReadOnly(false);
        //通过方法名匹配事务
        NameMatchTransactionAttributeSource nameMatchTransactionAttributeSource = new NameMatchTransactionAttributeSource();
        //为save方法添加事务,事务属性为defaultTransactionAttribute设置的属性
        nameMatchTransactionAttributeSource.addTransactionalMethod("save", defaultTransactionAttribute);
        //新建一个事务拦截器,使用platformTransactionManager作为事务管理器,拦截的方法为nameMatchTransactionAttributeSource中匹配到的方法
        return new TransactionInterceptor(platformTransactionManager, nameMatchTransactionAttributeSource);
    }


    @Bean
    public Advisor advisor() {
        AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
        //execution 表达式 匹配save方法
        aspectJExpressionPointcut.setExpression("execution(* com.shiyanlou.springboot..*.save(..))");
        //返回aop切面,切面=切点+通知
        return new DefaultPointcutAdvisor(aspectJExpressionPointcut, transactionInterceptor());
    }
}

总结(Summary)

重难点分析

FAQ分析

Bug分析