SpringBoot之整合AOP


文章目录

  • SpringBoot之整合AOP
  • 前言
  • 一、AOP是什么?
  • 二、AOP涉及术语
  • 三、系统日志记录
  • 1.引入依赖
  • 2.添加日志实体
  • 3.自定义日志标签
  • 4、添加切面类
  • 5、添加请求类
  • 6、发起请求
  • 四、统一平行权限处理


前言

主要通过整合AOP,完成对系统日志记录以及统一平行权限功能的实现

一、AOP是什么?

AOP:面向切面编程,相对于OOP面向对象编程 Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足。

二、AOP涉及术语

@Aspect切面类注解
@Poincut切点
Join point–连接点
Advice通知

常见通知:

  • before(前置通知): 在方法开始执行前执行
  • after(后置通知): 在方法执行后执行
  • afterReturning(返回后通知): 在方法返回后执行
  • afterThrowing(异常通知): 在抛出异常时执行
  • around(环绕通知): 在方法执行前和执行后都会执行

通知先后顺序:

around > before > around > after > afterReturning

三、系统日志记录

1.引入依赖

代码如下(示例):

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

2.添加日志实体

代码如下(示例):

package com.yangxf.si.core.logs;

import com.yangxf.si.core.entity.UuidEntityBase;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;


/**
 * 操作日志实体
 *
 * @author lin.wd
 */
@Entity
@Access(AccessType.FIELD)
@Table(name = "SYS_LOG_OPT")
@Data
@NoArgsConstructor
public class SysLogOpt extends UuidEntityBase <SysLogOpt, String> {

    private static final long serialVersionUID = -3659465267913789663L;
    /**
     * 日志类型
     *
     * @return
     */
    @Column(name="LOGTYPE")
    private String logType;

    /**
     * 类名
     *
     * @return
     */
    @Column(name="CLASSNAME")
    private String className;
    /**
     * 方法名
     *
     * @return
     */
    @Column(name="METHODNAME")
    private String methodName;
    /**
     * 参数
     *
     * @return
     */
    private String params;
    /**
     * 执行时间     
     *
     * @return
     */
    @Column(name="EXECTIME")
    private Long execTime;
    /**
     * 切面注释
     *
     * @return
     */
    private String remark;
    /**
     * 创建时间
     *
     * @return
     */
    @Column(name="CREATEDATE")
    private Long createDate;
    /**
     * 请求URL
     *
     * @return
     */
    private String url;
    /**
     * 请求IP
     *
     * @return
     */
    private String ip;
    /**
     * 请求方法
     *
     * @return
     */
    @Column(name="HTTPMETHOD")
    private String httpMethod;


    @Override
    public String getPK() {
        return super.getId();
    }

}

3.自定义日志标签

代码如下(示例):

package com.yangxf.si.modules.aspect;

import java.lang.annotation.*;

/**
 * 记录操作日志
 *
 * @Descrption 该注解是标签型注解,被此注解标注的方法需要进行查询日志记录
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {

    /**
     * @Description 查询方法名称
     */
    String value() default "";

    /**
     * @Description 系统类型
     */
    String sysType();
}

4、添加切面类

最常用的execution解释

  • 1、切自定义注解: @annotation(com.yangxf.si.modules.aspect.SysLog)
  • 2、切请求注解:- @annotation(org.springframework.web.bind.annotation.RequestMapping)
  • 3、execution(* com.yangxf.si.modules..(…))–切modules中所有类所有方法
  • 4、execution(* com.yangxf.si.modules.*.service.Service.save(String))–service包中,以Service为结尾的类,以save开头的方法,以String为参数
  • 5、execution(* com.yangxf.si.modules.*.service.Service.save(…))
  • 6、@Pointcut(“pointCut1() && pointCut2()”)
    可以使用 &&, ||, ! 运算符来定义切点
/**
 * FileName: SysLogAspect
 * Author:   Administrator
 * Date:     2021/3/1 10:08
 * Description: 系统日志记录切面类
 * History:
 * <author>          <time>          <version>          <desc>
 * 作者姓名           修改时间           版本号              描述
 */
package com.yangxf.si.modules.aspect;

import com.alibaba.fastjson.JSON;
import com.yangxf.si.core.logs.SysLogOpt;
import com.yangxf.si.core.logs.SysLogOptService;
import com.yangxf.si.core.utils.LongDateUtils;
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.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


/**
 * 请求记录日志 --记录操作日志@RecordLogRecordLog
 *
 * @author linwd
 */
@Aspect
@Component
public class SysLogAspect {

    private final static Logger logger = LoggerFactory.getLogger(SysLogAspect.class);

    @Autowired
    private SysLogOptService sysLogOptService;

    /**
     * 切入点
     * 最常用的execution解释
     * 1、切自定义注解: @annotation(com.yangxf.si.modules.aspect.SysLog)
     * 2、切请求注解:@annotation(org.springframework.web.bind.annotation.RequestMapping)
     * 3、execution(* com.yangxf.si.modules.*.*(..))--切modules中所有类所有方法
     * 4、execution(* com.yangxf.si.modules.*.service.*Service.save*(String))--service包中,以Service为结尾的类,以save开头的方法,以String为参数
     * 5、execution(* com.yangxf.si.modules.*.service.*Service.save*(..))
     * 6、@Pointcut("pointCut1() && pointCut2()")
     * 可以使用 &&, ||, ! 运算符来定义切点
     */
    @Pointcut("@annotation(com.yangxf.si.modules.aspect.SysLog)")
    public void logPointCut() {
    }

    /**
     * 前置通知
     *
     * @param point
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint point) {
        logger.info("SysLogAspect----doBefore");
    }

    /**
     * 环绕通知
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        logger.info("SysLogAspect----around");
        long beginTime = System.currentTimeMillis();
        Object result = point.proceed();
        long time = System.currentTimeMillis() - beginTime;
        try {
            saveLog(point, time);
        } catch (Exception e) {
            //日志保存是报错不抛出
        }
        return result;

    }

    /**
     * 保存日志
     *
     * @param joinPoint
     * @param time
     */
    private void saveLog(ProceedingJoinPoint joinPoint, long time) {
        SysLogOpt sysLogOpt = new SysLogOpt();
        sysLogOpt.setExecTime(time);//执行时间-时间戳
        /**
         * //获取请求url,ip,httpMethod        
         */
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteAddr();
        String httpMethod = request.getMethod();
        String url = request.getRequestURI().toString();
        sysLogOpt.setIp(ip);//请求IP
        sysLogOpt.setUrl(url);//请求URL
        sysLogOpt.setHttpMethod(httpMethod);//请求方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        sysLogOpt.setCreateDate(LongDateUtils.toSecondLong(new Date()));//创建时间--20210301204812
        Method method = signature.getMethod();
        SysLog sysLogAnnotation = method.getAnnotation(SysLog.class);
        if (sysLogAnnotation != null) {
            sysLogOpt.setRemark(sysLogAnnotation.value());//标签:方法名
            sysLogOpt.setLogType(sysLogAnnotation.sysType());//日志所在系统类型--多个系统公用一个库一个表时
        }

        /**
         * 请求类名、方法名
         */
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        sysLogOpt.setClassName(className);
        sysLogOpt.setMethodName(methodName);

        /**
         * 请求参数
         */
        Object[] args = joinPoint.getArgs();
        try {
            List <String> list = new ArrayList <>();
            for (Object object : args) {
                list.add(JSON.toJSON(object).toString());
            }
            sysLogOpt.setParams(list.toString());
        } catch (Exception e) {
            //
        }
        sysLogOptService.save(sysLogOpt);

    }


}

5、添加请求类

/**
 * FileName: HelloController
 * Author:   Administrator
 * Date:     2020/3/7 19:26
 * Description: 第一个
 * History:
 * <author>          <time>          <version>          <desc>
 * 作者姓名           修改时间           版本号              描述
 */
package com.yangxf.si.modules.helloworld;

import com.yangxf.si.modules.aspect.SysLog;
import lombok.extern.log4j.Log4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * 〈测试文件〉<br>
 * 〈第一个创建文件〉
 *
 * @author lin.wd
 * @create 2020/3/7
 * @since 1.0.0
 */
@RestController
@Log4j
public class HelloController {


    @SysLog(value="日志记录测试")
    @GetMapping("/hello")
    public String hello() {
        log.info("Hello world");
        return "Hello world";
    }
}

6、发起请求

http://localhost:8080/api/hello

spring boot AOP详解 springboot aop around_aop


spring boot AOP详解 springboot aop around_spring boot AOP详解_02

四、统一平行权限处理

/**
 * FileName: AllRequestAspect
 * Author:   Administrator
 * Date:     2021/3/1 21:27
 * Description: 统一平行权限设定切面类
 * History:
 * <author>          <time>          <version>          <desc>
 * 作者姓名           修改时间           版本号              描述
 */
package com.yangxf.si.modules.aspect;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * 〈一句话功能简述〉<br>
 * 〈统一平行权限设定切面类〉
 *
 * @author Administrator
 * @create 2021/3/1
 * @since 1.0.0
 */
@Component
@Aspect
public class AllRequestAspect {

    /*** 日志 */
    private final static Logger logger = LoggerFactory.getLogger(AllRequestAspect.class);
    private static final String NOT_ALLOWED_MSG = "统一平行权限校验,不允许访问";

    /*** 处理状态 */
    enum AllowedStatusEnum {
        ALLOWED, NOT_ALLOWED
    }

    // 切全部请求
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void requestPointcut() {
    }

    private String getKey(String parameterName) {
        if ("personId".equalsIgnoreCase(parameterName)) {
            logger.debug("统一平行权限处理,校验根据参数 {}", parameterName);
            return "id";
        }
        if ("idNumber".equalsIgnoreCase(parameterName)) {
            logger.debug("统一平行权限处理,校验根据参数 {}", parameterName);
            return "idNumber";
        }
        return "";
    }

    @Before("requestPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        if (logger.isDebugEnabled()) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            logger.debug(">>>请求路径 {} 处理类 {} 处理方法 {}", request.getRequestURI(), joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName());
        }
        AllowedStatusEnum allowedStatus = AllowedStatusEnum.NOT_ALLOWED;
        //请求参数值
        Object[] args = joinPoint.getArgs();
        //请求参数名称
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] parameterNames = methodSignature.getParameterNames();
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg == null) {
                continue;
            }
            if (isJavaClass(arg.getClass())) {
                String parameterName = parameterNames[i];
                String key = getKey(parameterName);
                if (StringUtils.isNotEmpty(key)) {
                    allowedStatus = allowedForKey(key, args[i]);
                }
            } else {
                String[] filedName = getFiledName(arg);
                for (String filed : filedName) {
                    String key = getKey(filed);
                    if (StringUtils.isNotEmpty(key)) {
                        allowedStatus = allowedForKey(key, getFieldValueByName(filed, arg));
                    }
                }
            }
        }

        if (allowedStatus == AllowedStatusEnum.NOT_ALLOWED) {
            logger.warn("统一平行权限处理,未涉及到该请求,请处理平行权限!!!");
        } else if (allowedStatus == AllowedStatusEnum.ALLOWED) {
            logger.debug("统一平行权限处理,校验通过");
        } else if (allowedStatus == AllowedStatusEnum.NOT_ALLOWED) {
            logger.error(NOT_ALLOWED_MSG);
        }
    }


    public AllowedStatusEnum allowedForKey(String key, Object value) {
        //value为空,无法校验
        if (value == null) {
            return AllowedStatusEnum.ALLOWED;
        }

        /**
         * 结合认证项目的SpringSecurity实现权限管理
         * 获取缓存中登录成功后,缓存的用户信息,对比,如果不是当前登录用户,不允许请求
         */

        return AllowedStatusEnum.ALLOWED;
    }

    /**
     * 判断一个类是JAVA类型还是用户定义类型
     *
     * @param clz
     * @return
     */
    public static boolean isJavaClass(Class <?> clz) {
        return clz != null && clz.getClassLoader() == null;
    }

    /**
     * 获取属性名数组
     */
    private String[] getFiledName(Object o) {
        Field[] fields = o.getClass().getDeclaredFields();
        String[] fieldNames = new String[fields.length];
        for (int i = 0; i < fields.length; i++) {
            fieldNames[i] = fields[i].getName();
        }
        return fieldNames;
    }


    /**
     * 根据属性名获取属性值
     */
    private Object getFieldValueByName(String fieldName, Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter, new Class[]{});
            Object value = method.invoke(o, new Object[]{});
            return value;
        } catch (Exception e) {
            return null;
        }
    }

}