Spring AOP面向切面编程(代理模式)

AOP(Aspect Oriented Programming):面向切面编程

通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

  • 利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性。
  • 在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现业务逻辑。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
  • 将公共功能提取出来可以极大的提升开发效率 同时提高了开发的效率。
  • 以往是通过继承体系来达到代码重用,如果将继承体系看做一种自上而下的树状结构,那么继承是一种纵向的代码重用,随着时间推移软件行业又提出了aop的概念及面向切片编程,aop可以看做是横向的代码重用;

主要功能:

将共用性系统功能(日志、性能、安全、异常等等)和其他共用功能(数据验证,加解密)和业务逻辑分离;

日志记录,性能统计,安全控制,事务处理,异常处理,数据验证,数据加解密,为了保证对象在并发下只有一个对象访问的加锁和解锁等等。

首先日志、数据验证、加解密这些代码不应该写在需求中,这样代码重复混乱臃肿不灵活,也会带来很大的工作量。比如日志需求发生变化,就要修改所有的模块。

AOP和OOP(Object Oriented Programming)的区别:

OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果。

如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话;那么面向切面编程则是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。

面向对象编程主要用于为同一对象层次的公用行为建模。它的弱点是将公共行为应用于多个无关对象模型之间。而这恰恰是面向切面编程适合的地方。有了 AOP,我们可以定义交叉的关系,并将这些关系应用于跨模块的、彼此不同的对象模型。AOP 同时还可以让我们层次化功能性而不是嵌入功能性,从而使得代码有更好的可读性和易于维护。它会和面向对象编程合作得很好。 oop对象层面建模,AOP业务层面建模

原理

AOP是使用JDK动态代理和cglib动态代理技术来实现的,AOP基于动态代理模式,对方法功能进行增强(是方法级别的)

  1. JDK动态代理:通过实现InvocationHandlet接口,并重写里面的invoke方法,通过为proxy类指定classLoader和一组interfaces来创建动态代理
  2. cglib的动态代理:CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

AOP进行增强的方法,该方法的类如果有接口则使用JDK实现动态代理,没有接口则使用CGLib

JDK实现动态代理需要实现类通过接口定义业务方法,CGLib的动态代理是通过继承类来实现;

import net.sf.cglib.proxy.Enhancer;

 //CGLib实现动态代理
public class Client {
	public static void main(String[] args) {
		Enhancer enhancer=new Enhancer();
		enhancer.setSuperclass(HelloServiceImlpl.class);//继承被代理类
		enhancer.setCallback(new HelloMethodInterceptor());//设置回调
		HelloServiceImlpl helloService=(HelloServiceImlpl) enhancer.create();//生成代理类对象
		helloService.sayHello();//在调用代理类方法时会被我们实现的方法拦截器拦截
	}
}

切面(Aspect)

切面(Aspect)是在面向切面编程(AOP)中的一个重要概念。它包含了与特定关注点相关的通知和切点的组合,用于定义在何时、何地以及如何应用通知。切面可以看作是横切关注点(cross-cutting concern)的模块化封装。

切面包含了特定关注点的通知和切点的一个模块化封装

在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。

切面与其他概念之间的关系如下:

  • 切点定义了一组连接点,表示切面将被织入的具体位置。
  • 通知描述了切面在连接点处执行的动作。
  • 引入允许为现有类添加新的方法或属性,通过引入可以实现切面的功能扩展。
  • 织入则是将切面应用到目标对象并创建新的代理对象的过程。

切面包含以下内容:

  • 连接点(Join point):指程序执行过程中可以插入切面的一个特定点。例如,在方法调用、方法执行、异常抛出等时刻都是连接点。
  • 切点(Pointcut):指一组连接点的集合,它定义了切面将被织入的具体位置。切点使用表达式来匹配连接点,从而确定在哪些连接点上应用切面。
  • 通知(Advice):指切面在特定连接点处执行的动作。通知有不同的类型,如前置通知(Before advice)、后置通知(After advice)、环绕通知(Around advice)、返回通知(After returning advice)和异常通知(After throwing advice)等。
  • 引入(Introduction):允许在现有的类中添加新的方法或属性。引入使得一个类具备额外的功能,而无需修改源代码。
  • 织入(Weaving):将切面应用到目标对象并创建新的代理对象的过程称为织入。织入可以在编译时、类加载时或运行时进行,Spring AOP采用的是运行时织入。

通过使用Spring AOP,可以在运行时动态地获取和操作这些内容,以实现横切关注点的功能,如日志记录、性能监控、事务管理等。同时,Spring提供了丰富的注解和配置选项,使得配置和使用AOP变得简洁和灵活。

// 目标对象(Target Object)就是被代理对象,被一个或者多个切面所通知。
// 目标对象
public class UserService {
    public void saveUser(String name) {
        System.out.println("Saving user: " + name);
    }
}

// 切面类
public class LoggingAspect {
    // 前置通知,在目标方法执行之前执行
    public void beforeAdvice() {
        System.out.println("Before advice executed");
    }
    
    // 后置通知,在目标方法执行之后执行
    public void afterAdvice() {
        System.out.println("After advice executed");
    }
}

// 使用Spring AOP配置切面和目标对象
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        UserService userService = (UserService) context.getBean("userService");
        userService.saveUser("John Doe"); // 在切点处调用目标方法
        
        ((ClassPathXmlApplicationContext) context).close();
    }
}

<!-- applicationContext.xml -->
<beans>
    <bean id="userService" class="com.example.UserService" />
    <bean id="loggingAspect" class="com.example.LoggingAspect" />

    <!-- 配置切面 -->
    <aop:config>
        <aop:aspect ref="loggingAspect">
            <!-- 定义切点,匹配UserService类的saveUser方法 -->
            <aop:pointcut id="saveUserPointcut" expression="execution(* com.example.UserService.saveUser(..))" />
            
            <!-- 应用前置通知到切点 -->
            <aop:before method="beforeAdvice" pointcut-ref="saveUserPointcut" />
            
            <!-- 应用后置通知到切点 -->
            <aop:after method="afterAdvice" pointcut-ref="saveUserPointcut" />
        </aop:aspect>
    </aop:config>
</beans>

在上述示例中,LoggingAspect类是一个切面,它包含了前置通知和后置通知两个通知。这些通知定义了在目标对象的特定连接点(切点)处执行的逻辑。通过配置文件中的<aop:config><aop:aspect>标签,将切面与目标对象关联起来,并通过切点指定应用通知的位置。在程序运行时,Spring AOP会将切面织入目标对象的调用流程中,从而实现日志记录的功能。

切入点表达式

1、execution表达式

用于匹配方法执行的连接点,属于方法级别

语法
execution(修饰符 返回值类型 方法名(参数)异常)

语法参数描述
修饰符可选,如public,protected,写在返回值前,任意修饰符填*号就可以
返回值类型必选,可以使用*来代表任意返回值
方法名必选,可以用*来代表任意方法
参数()代表是没有参数,(..)代表是匹配任意数量,任意类型的参数,当然也可以指定类型的参数进行匹配,如要接受一个String类型的参数,则(java.lang.String), 任意数量的String类型参数:(java.lang.String..)等等。。。
异常可选,语法:throws 异常,异常是完整带包名,可以是多个,用逗号分隔

符号

符号描述
*匹配任意字符
..匹配多个包或者多个参数
+表示类及其子类

条件符

符号描述
&&、and
!

案例

  • 拦截com.gj.web包下的所有子包里的任意类的任意方法
    execution(* com.gj.web..*.*(..))
  • 拦截com.gj.web.api.Test2Controller下的任意方法
    execution(* com.gj.web.api.Test2Controller.*(..))
  • 拦截任何修饰符为public的方法
    execution(public * * (..))
  • 拦截com.gj.web下的所有子包里的以ok开头的方法
    execution(* com.gj.web..*.ok*(..))\

2、@annotation和@Within

用于匹配类和方法

@within和@annotation的区别:
@within 对象级别
@annotation 方法级别

@Around("@within(org.springframework.web.bind.annotation.RestController") 
        这个用于拦截标注在类上面的@RestController注解

@Around("@annotation(org.springframework.web.bind.annotation.RestController") 
         这个用于拦截标注在方法上面的@RestController注解

三、AOP通知

在切面类中需要定义切面方法用于响应响应的目标方法,切面方法即为通知方法,通知方法需要用注解标识,AspectJ支持5种类型的通知注解

注解描述
@Before前置通知, 在方法执行之前执行
@After后置通知, 在方法执行之后执行
@AfterReturn返回通知, 在方法返回结果之后执行
@AfterThrowing异常通知, 在方法抛出异常之后
@Around环绕通知,围绕方法的执行

AOP在Spring框架中应用非常广泛,我们平时最常见的事务,就是使用AOP来实现的;在方法前开启事务,方法后提交事务,检测到有异常时,回滚事务...

前置增强校验参数

如果需要在执行方法addCustomer之前,我们进行一些其它的业务操作,比如校验参数是否为空,这时候就可以使用前置增强Before来实现了;获取请求参数信息的代码如下

public class AopHelper {
    /**
     *  获取请求方法的参数
     * @param joinPoint
     */
    public  static Map getMethodParams(JoinPoint joinPoint){
        String[] names = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        Map params =new HashMap();
        if (ArrayUtils.isEmpty(names)) return params;
        Object[] values = joinPoint.getArgs();
        for (int i = 0; i < names.length; i++) {
            params.put(names[i],values[i]);
        }
        return params;
    }
}

定义一个AOP,在AOP中配置前置增强及拦截规则

@Slf4j
@Aspect
@Component
public class MyAop {
    /**
     * 测试前置增强,测试参数非空校验,此方法还可完善为携带有注定注解的参数则校验非空校验,不携带则不校验
     * 测试通过 正则匹配 的方式使用AOP
     */
    @Before(value ="execution(public * com.aop.service.*.*(..))")
    public void before1(JoinPoint joinPoint){
        Map params =  AopHelper.getMethodParams(joinPoint);
        params.forEach((key,value)->{
            if (Objects.isNull(value)) throw new RuntimeException("参数" + key + "不能为空");
        });
        log.info("【测试前置增强:】"+joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName());
    }
}

后置增强保存日志

可以在方法运行之后进行一些逻辑处理,比如打印一个日志告知当前方法运行成功;在MyAop这个AOP类中,新增一个后置增强的代码

/**
 * 测试后置增强,测试使用target指定目标对象
 */
@After(value ="target(com.aop.service.CustomerService)")
public void after(JoinPoint joinPoint){
    log.info("后置增强AfterAop测试指定目标匹配 " + joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName() + "调用成功");
}

环绕增强

环绕增强是在调用业务方法之前和调用业务方法之后都可以执行响应的增强语法,也就是说:一个前置增强和一个后置增强相当于是组成了一个环绕增强;不同的是,在前置增强和后置增强中,在AOP中前置增强和后置增强只能拿到JoinPoint类,而在环绕增强中,可以拿到ProceedingJoinPoint类;

ProceedingJoinPoint类继承了JoinPoint类,也继承了JoinPoint所有的非私有的方法;比如获取连接点相关信息、获取参数信息、获取方法等等,并且ProceedingJoinPoint类扩展了JoinPoint类的方法,ProceedingJoinPoint可以调用业务方法执行业务逻辑,而JoinPoint则不可以;

也就是在环绕增强中,可以执行业务方法,而在前置增强和后置增强中则不可以;这里可以通过环绕增强实现数据库事务的实现,也可以通过环绕增强实现程序运行时间的记录;

新建一个注解RunTimeLog,测试使用注解的方法建立匹配AOP规则

/**
 * @demand: 定义记录方法执行时间的注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunTimeLog {
}

在MyAop中新增方法

/**
 * 测试环绕增强,环绕增强参数可以为ProceedingJoinPoint,可以调用业务方法
 * 通过注解的形式进行AOP测试
 */
@Around("@annotation(com.aop.conf.RunTimeLog)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    log.info("【进入环绕增强】触发环绕那个增强开始");
    long startTime = System.currentTimeMillis();
    Object result = null;
    // 通过代理类调用业务逻辑执行
    result = pjp.proceed();
    log.info("【测试环绕增强:】当前方法{}执行成功,调用者为:{},  此方法运行时间为:{} ms", pjp.getSignature().getName(), pjp.getTarget().getClass().getName(), (System.currentTimeMillis() - startTime));
    return result;
}

在类CustomerService的addCustomer方法上添加刚刚定义的注解@RunTimeLog,去掉业务方法中的10/0后再次运行程序;通过控制台输出可以观察到:

【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
【测试环绕增强:】当前方法addCustomer执行成功,调用者为:com.aop.service.CustomerService,  此方法运行时间为:15 ms
 后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功

由此可以看出,在同一个AOP中,前置增强和后置增强以及环绕增强的先后顺序为:

Around --> Before --> Around --> After

最终增强

关于最终增强,这个和后置增强有一点类似,都是在业务方法执行后执行,不过两者还是有差异的 最终增强的顺序比后置增强要小,也就是:Around --> Before --> Around --> After --> AfterReturning

此时,可以观察到,程序进入业务方法后,有抛出异常,然后后置增强还是正常执行了,不过,此时最终增强是没有被触发的;这就是两者的区别之一;如果用代码来描述这种关系,更多的有点类似下面这种

// 前置增强
try {
    // TODO 业务方法
    // 最终增强
}catch (Exception e){
    e.printStackTrace();
    // 异常增强
}finally {
    // 后置增强
}

最终增强暂时还没有想到在哪些地方有应用场景;

异常增强

异常增强即在业务方法调用是程序出现异常并且异常在没有被捕获的前提下所触发的,我们可以使用最终增强来记录程序的错误日志,以便于我们进行排错等; 在MyAop类中新增方法

/**
 *  测试异常增强,
 */
@AfterThrowing(value = "execution(public * com.aop.service.*.*(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
    Map params =  AopHelper.getMethodParams(joinPoint);
    log.info("触发异常增强,当前程序抛出异常的方法是:" + joinPoint.getSignature()
            + ", 请求参数为:" + params.toString() + ",异常信息为:" + e.getMessage());
    // TODO 后面可执行入库、入ELK、入mongoDB等等
}
【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.aop.service.CustomerService.addCustomer(CustomerService.java:29)
后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功
	at com.aop.service.CustomerService$$FastClassBySpringCGLIB$$ef7074c5.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
触发异常增强,当前程序抛出异常的方法是:String com.aop.service.CustomerService.addCustomer(Long,String,String), 请求参数为:{address=一人之下, customerId=1234, userName=冯宝宝},异常信息为:/ by zero
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)

异常增强被触发,可以获取到当前请求的参数信息、时间信息等,记录到数据库或者是ELK等,以便于排错使用,并且这些逻辑和业务代码是分开的互不影响,很大程度上的解除代码耦合;

同时,当业务方法中没有异常信息时,则不会触发异常增强;从这里可以看出在同一个AOP中的执行顺序 Around --> Before --> Around --> After --> AfterReturning --> AfterThrowing

@AfterReturn 其中value表示切点方法,returning表示返回的结果放到result这个变量中\

        /**
         * returning属性指定连接点方法返回的结果放置在result变量中
         * @param joinPoint 连接点
         * @param result 返回结果
         */
        @AfterReturning(value = "testCut()",returning = "result")
        public void afterReturn(JoinPoint joinPoint, Object result) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            System.out.println("注解方式AOP拦截的方法执行成功, 进入返回通知拦截, 方法名为: "+method.getName()+", 返回结果为: "+result.toString());
        }

@AfterThrowing:其中value表示切点方法,throwing表示异常放到e这个变量\

        @AfterThrowing(value = "testCut()", throwing = "e")
        public void afterThrow(JoinPoint joinPoint, Exception e) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            System.out.println("注解方式AOP进入方法异常拦截, 方法名为: " + method.getName() + ", 异常信息为: " + e.getMessage());
        }

四、SpringBoot中使用

//创建一个自定义注解类CacheableTest
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface CacheableTest {
    String key();
    String value() default "";
    int expireTime() default 3600;
}

//创建一个controller类,并加入该方法:
@RequestMapping("/findPage2")
@CacheableTest(key="haha",value = "hehe")
public String findPage2(Integer pageNumber, Integer pageSize){
    System.out.println("findPage2请求成功!!!");
    return "请求成功";
}

//创建一个切面类TestAop:
@Component
@Aspect
public class TestAop {


    @Pointcut("execution(public * com.wang.controller.TestController.*(..))")    //第一个星号指返回值类型为任意
    private void pointCut(){};


    @Before(value = "pointCut()")
    public void logBefore(JoinPoint joinpoint) {
        System.out.println("----------Before开始-----------");

        System.out.println("方法名:"+ joinpoint.getSignature().getName());
        System.out.println("参数值集合:"+ Arrays.asList(joinpoint.getArgs()));
        System.out.println("参数值类型:"+ joinpoint.getArgs()[0].getClass().getTypeName());
        //获取目标注解对象,CacheableTest是自定义的一个注解
        CacheableTest cacheable = ((MethodSignature)joinpoint.getSignature()).getMethod().getAnnotation(CacheableTest.class);
        String classType = joinpoint.getTarget().getClass().getName();
        System.out.println("目标注解对象:"+ cacheable);
        System.out.println("获取目标方法所在类:"+ classType);

        System.out.println("----------Before结束-----------");
    }

    @After(value = "pointCut()")
    public void logAfter(JoinPoint joinpoint) {
        System.out.println("---------After开始------------");

        System.out.println("---------After结束-------------");
    }

}

AOP实现并发对共享数据访问

  1. 乐观锁,如果是数据库中的数据添加版本号比较;
  2. 提供一些必备的功能,对被访问对象实现加锁或解锁功能。以保证所有在修改数据对象的操作之前能够调用lock()加锁,在它使用完成后,调用unlock()解锁。 下面是一个示例代码,展示如何使用AOP来实现并发对共享数据的访问,并添加乐观锁以及加锁和解锁的功能。

首先,我们创建一个共享数据对象 SharedData,它包含一个数据字段 data 和一个版本号字段 version

public class SharedData {
    private String data;
    private int version;

    public SharedData(String data, int version) {
        this.data = data;
        this.version = version;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}

接下来,我们定义切面类 ConcurrencyAspect,用于实现乐观锁和加锁/解锁功能。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;

@Aspect
public class ConcurrencyAspect {

    @Before("execution(* com.example.SharedData.setData(..)) && args(data)")
    public void lock(String data) {
        // 加锁逻辑,可以在这里实现自定义的加锁操作
        System.out.println("Locking data: " + data);
    }

    @After("execution(* com.example.SharedData.setData(..)) && args(data)")
    public void unlock(String data) {
        // 解锁逻辑,可以在这里实现自定义的解锁操作
        System.out.println("Unlocking data: " + data);
    }

    @Before("execution(* com.example.SharedData.*(..)) && !execution(* com.example.SharedData.setData(..))")
    public void optimisticLock() {
        // 乐观锁逻辑,可以在这里实现自定义的版本号比较和处理
        System.out.println("Performing optimistic lock");
    }
}

最后,我们使用 Spring AOP 将切面与目标对象 SharedData 关联起来,并进行测试。

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@EnableAspectJAutoProxy
public class Application {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);

        SharedData sharedData = context.getBean(SharedData.class);
        sharedData.setData("Hello World");

        ((AnnotationConfigApplicationContext) context).close();
    }
}

通过上述示例,我们实现了对共享数据的并发访问控制。切面类 ConcurrencyAspect 中的 lock() 方法和 unlock() 方法提供了加锁和解锁的功能,而 optimisticLock() 方法则实现了乐观锁的逻辑。在运行时,Spring AOP 会将切面织入到 SharedData 对象的方法调用中,从而实现并发访问控制和乐观锁的功能。

SpringBoot项目如何优雅的实现操作日志记录

在实际开发当中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。 通常就是使用Spring中的AOP特性来实现的,那么在SpringBoot项目当中应该如何来实现呢?

AOP(Aspect-Oriented Programming:⾯向切⾯编程),说起AOP,几乎学过Spring框架的人都知道,它是Spring的三大核心思想之一(IOC:控制反转,DI:依赖注入,AOP:面向切面编程)。能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

简单说来,AOP主要做三件事:

1、在哪里切入,也就是日志记录等非业务代码在哪些业务代码中执行。 2、在什么时候切入,是在业务代码执行前还是后。 3、切入后做什么事情,比如权限校验,日志记录等。

可以用一张图来理解: image

图上的一个核心术语的说明:

  1. Pointcut:切点,决定在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。execution方式:可以用路径表达式指定哪些类织入切面,annotation方式:可以指定被哪些注解修饰的代码织入切面。
  2. Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
  3. Aspect:切面,即Pointcut和Advice。
  4. Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
  5. Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

三、实现步骤

(1)自定义一个注解@Log (2)创建一个切面类,切点设置为拦截标注@Log的方法,截取传参,进行日志记录 (3)将@Log标注在接口上 具体的实现步骤如下:

1. 添加AOP依赖

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

2. 自定义一个日志注解

日志一般使用的是注解类型的切点表达式,我们先创建一个日志注解,当spring容器扫描到有此注解的方法就会进行增强。代码如下(示例):

@Target({ ElementType.PARAMETER, ElementType.METHOD }) // 注解放置的目标位置,PARAMETER: 可用在参数上  METHOD:可用在方法级别上
@Retention(RetentionPolicy.RUNTIME)    // 指明修饰的注解的生存周期  RUNTIME:运行级别保留
@Documented
public @interface Log {

    /**
     * 模块
     */
    String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;
}

3. 切面声明

申明一个切面类,并交给Spring容器管理。代码如下(示例):


@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private IXlOperLogService operLogService;

    /**
     * 处理完请求后执行
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturnibng(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            // 获取当前的用户
            JwtUser loginUser = SecurityUtils.getLoginUser();

            // 日志记录
            XlOperLog operLog = new XlOperLog();
            operLog.setStatus(0);
            // 请求的IP地址
            String iP = ServletUtil.getClientIP(ServletUtils.getRequest());
            if ("0:0:0:0:0:0:0:1".equals(iP)) {
                iP = "127.0.0.1";
            }
            operLog.setOperIp(iP);
            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            if (loginUser != null) {
                operLog.setOperName(loginUser.getUsername());
            }
            if (e != null) {
                operLog.setStatus(1);
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            operLog.setOperTime(new Date());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 保存数据库
            operLogService.save(operLog);

        } catch (Exception exp) {
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     * @param log 日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, XlOperLog operLog, Object jsonResult) throws Exception {
        // 设置操作业务类型
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 设置参数的信息
            setRequestValue(joinPoint, operLog);
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, XlOperLog operLog) throws Exception {
        String requsetMethod = operLog.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requsetMethod) || HttpMethod.POST.name().equals(requsetMethod)) {
            String parsams = argsArrayToString(joinPoint.getArgs());
            operLog.setOperParam(StringUtils.substring(parsams,0,2000));
        } else {
            Map<?,?> paramsMap = (Map<?,?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            operLog.setOperParam(StringUtils.substring(paramsMap.toString(),0,2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object object : paramsArray) {
                // 不为空 并且是不需要过滤的 对象
                if (StringUtils.isNotNull(object) && !isFilterObject(object)) {
                    Object jsonObj = JSON.toJSON(object);
                    params += jsonObj.toString() + " ";
                }
            }
        }
        return params.trim();
    }

    /**
     * 判断是否需要过滤的对象。
     * @param object 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object object) {
        Class<?> clazz = object.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) object;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) object;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return object instanceof MultipartFile || object instanceof HttpServletRequest
                || object instanceof HttpServletResponse || object instanceof BindingResult;
    }
}

4. 标注在接口上

将自定义注解标注在需要记录操作日志的接口上,代码如下(示例):

	@Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @ApiOperation(value = "批量生成代码")
    @GetMapping("/download/batch")
    public void batchGenCode(HttpServletResponse response, String tables) throws IOException {
        String[] tableNames = Convert.toStrArray(tables);
        byte[] data = genTableService.downloadCode(tableNames);
        genCode(response, data);
    }

5. 实现的效果

执行相关操作就会记录日志,记录了一些基础信息存在数据表里。 image

全部评论

相关推荐

03-09 20:32
运营
牛客972656413号:成绩管理系统会不会有点太。。。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务