Aop(面向切面编程):

springboot 切面获取参数 自定义注解 springboot的切面_java

借用一图,我自己浅显的理解为将许多简单,但是有用到的方法抽取出来,降低代码耦合度,更关注与核心功能。例如,对于常用的日志功能,我们可以不用在每个模块中重复的日志代码,而是使用动态代理的原理:使用一个代理将对象包裹起来,之后每次对这个对象的调用都要通过这个代理。

1、AOP概念及注解

切面(Aspect):一个关注点的模块化。以注解@Aspect的形式放在类上方,声明一个切面。

(Pointcut和Advice的结合)

连接点(Joinpoint):在程序执行过程中某个特定的点,比如某个方法调用或者处理异常时候都可以是连接点。

切点(Pointcut):筛选出的连接点,一个类中的所有方法都是连接点,但是又不全需要,会筛选出某些作为连接点作为切点。如果说通知定义了切面的动作或执行时机的话,切点则定义了执行的低点。切点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

两种方式:一种是注解,一种是切点表达式execution(…)

引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让他们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去。

目标对象(Target Object):被一个或者多个切面所通知的对象。也被称作被(adviced)对象。既然Spring AOP是用过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。

AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

织入(Weaving):把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知(Advice):通知增强,需要完成的工作叫做通知,就是业务逻辑中需要的比如事务、日志等先定义好,然后再需要的地方使用。

@Aspect:将当前类作为一个切面类

springboot 切面获取参数 自定义注解 springboot的切面_后端_02


@Component:Spring注解

@Pointcut:切点

注解的方式,定义我们的匹配规则,括号中的是我们自定义的注解的路径

springboot 切面获取参数 自定义注解 springboot的切面_java_03


springboot 切面获取参数 自定义注解 springboot的切面_spring_04


@Around:属于环绕增强,能控制切点执行前后,使用该注解后,当程序抛出异常会影响@AfterThrowing注解(环绕通知,可以同时在所拦截方法的前后,执行一段逻辑)。

@Before:前置通知,在切点方法之前执行(在所拦截方法执行之前执行一段逻辑)。

通过JoinPoint参数获取目标方法的方法名、修饰符等信息。

@After:后置通知,在切点方法之后执行。

@AfterReturning:返回通知,切点方法返回后执行。在该方法中可以获取目标方法返回值。returning参数是指返回值的变量名,对应方法的参数。

@AfterThrowing:异常通知,切点方法抛异常执行。

2、日志功能

2.1、mybatis-plus 代码生成

(1)数据库建表:
CREATE TABLE `sys_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `user_action` varchar(255) NOT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
(2)导入相关依赖(springboot):
<!--mybaits-plus依赖->
<dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.3.1.tmp</version>
</dependency>
<!--代码生成器--!>
<dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-generator</artifactId>
      <version>3.3.1.tmp</version>
</dependency>
<!--模板引擎--!>
<dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
      <version>2.3.29</version>
</dependency>

(3)aop依赖:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(3)代码生成类

官网链接 我自己的代码:

此处要注意,假如要生成id,一定要删除

springboot 切面获取参数 自定义注解 springboot的切面_java_05


数据源配置注意路径和登陆的账号密码

package com.example.wxs.aicode;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @author cmy 2020.03.04
 */
public class CodeGenerator {
    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("cmy");
        gc.setOpen(false);
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/wxdatas?serverTimezone=UTC&characterEncoding=utf-8&serverTimezone=Asia/Shanghai");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(scanner("模块名"));
        pc.setParent("com.example.wxs");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 写于父类中的公共字段
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

自动生成的目录如下:

springboot 切面获取参数 自定义注解 springboot的切面_spring_06


注意假如你的项目有多个mapper包,请在启动类上一定要扫描到,

springboot 切面获取参数 自定义注解 springboot的切面_java_07

(4)自定义注解

启动类同级的包下新建

package com.example.wxs.configs;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//说明注解所修饰的范围,这边是用在方法
@Target(ElementType.METHOD)
//注解的注解,被它修饰的注解的生命周期,这边是指被修饰的注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}
(5)服务层接口和实现类
package com.example.wxs.aoplog.service;

import com.example.wxs.aoplog.entity.SysLog;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author cmy
 * @since 2020-03-04
 */

public interface ISysLogService extends IService<SysLog> {
    /**
     * 插入日志
     * @param entity
     * @return
     */
    int insertLog(SysLog entity);
}
package com.example.wxs.aoplog.service.impl;

import com.example.wxs.aoplog.entity.SysLog;
import com.example.wxs.aoplog.mapper.SysLogMapper;
import com.example.wxs.aoplog.service.ISysLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author cmy
 * @since 2020-03-04
 */
@Service
public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> implements ISysLogService {

    @Autowired
    private SysLogMapper sysLogMapper;

    @Override
    public int insertLog(SysLog entity) {
        // TODO Auto-generated method stub
        return sysLogMapper.insert(entity);
    }
}

2.2、切面类

启动类同级包下新建

package com.example.wxs.configs;

import com.example.wxs.aoplog.entity.SysLog;
import com.example.wxs.aoplog.service.impl.SysLogServiceImpl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;

/**
 * @author cmy 2020.03.04
 */
@Aspect
@Component
public class LogAsPect {

    private final static Logger log=org.slf4j.LoggerFactory.getLogger(LogAsPect.class);

    @Autowired
    private SysLogServiceImpl sysLogService;

    @Pointcut("@annotation(com.example.wxs.configs.Log)")
    public void pointcut() {}

    @Around("pointcut()")
	/*Proceedingjoinpoint 继承了JoinPoint ,在JoinPoint 的基础上暴露了proceed()方法,暴露
	*出这个方法,就能支持 aop:around 这种切面,也就是说Proceedingjoinpoint 才支持环绕通知
	*/
    public Object around(ProceedingJoinPoint point){
        Object result=null;
        long beginTime = System.currentTimeMillis();
        try{
            log.info("我在目标方法之前执行!");
             //启动目标方法执行
            result=point.proceed();
            long endTime = System.currentTimeMillis();
            insertLog(point,endTime-beginTime);
        }catch (Throwable e){

        }
        return result;
    }
	
	//插入到之前的日志表中
    private void insertLog(ProceedingJoinPoint point,long time){
        //获取方法(MethodSignature主要实现的是返回值类,方法名和形式参数)
        MethodSignature signature=(MethodSignature)point.getSignature();
        Method method=signature.getMethod();
        SysLog sys_log=new SysLog();
        //如果存在这样的注释,则返回指定类型的元素的注释
        //例如,@Log("插入人员"),userAction ="插入人员"
        Log userAction =method.getAnnotation(Log.class);
        if(userAction!=null){
            sys_log.setUserAction(userAction.value());
        }
        //获取被代理的对象,通过反射获取类名
        String className=point.getTarget().getClass().getName();
        String methodName=signature.getName();
        //获取方法的参数
        String args= Arrays.toString(point.getArgs());

        int userid=1;
        sys_log.setUserId(userid);
        sys_log.setCreateTime(new java.sql.Timestamp(new Date().getTime()));
        log.info("登陆人:{},类名:{},方法名:{},参数:{},执行时间:{}",userid, className, methodName, args, time);
        sysLogService.insertLog(sys_log);

    }

    @Pointcut("execution(public * com.example.wxs.controller..*.*(..))")
    public void pointcutController(){

    }

    @Before("pointcutController()")
    public void around2(JoinPoint point){
    //获取目标方法路径/名和参数
        String methodNam = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
        String params = Arrays.toString(point.getArgs());
        log.info("get in {} params :{}",methodNam,params);
    }
}

@“execution(* ….(…))”
表示匹配所有方法
@execution(public * com.example.wxs.controller….(…))
表示匹配com.example.wxs.controller所有公有方法
@execution(* com.example.wxs….(…))
表示匹配com.example.wxs包及其子包下的所有方法

测试

①、用户执行插入一条记录的功能,日志表插入一条数据

在服务层的实现类上加注解@Log,对应之前新建的自定义注解Log。

切面类中的代码:

springboot 切面获取参数 自定义注解 springboot的切面_后端_08


实现类中的代码:

springboot 切面获取参数 自定义注解 springboot的切面_java_09


效果:

springboot 切面获取参数 自定义注解 springboot的切面_AOP_10


②、用户执行插入一条记录的功能,开发者控制台输出

springboot 切面获取参数 自定义注解 springboot的切面_AOP_11


com.example.wxs.controller所有公有方法执行之前都会打印。

效果:

springboot 切面获取参数 自定义注解 springboot的切面_AOP_12