SpringBoot-AOP

x33g5p2x  于2021-09-24 转载在 Spring  
字(44.1k)|赞(0)|评价(0)|浏览(587)

AOP(Aspect-Oriented Programming):面向切面编程,不改动原来代码,运行时动态增强。

1、使用场景

  1. 系统日志处理
  2. 系统事务处理
  3. 系统安全验证
  4. 系统数据缓存

2、优点

  1. 在不改变原有功能代码的基础上扩展新的功能实现——OCP原则。
  2. 可以简化代码开发提高效率。
  3. 可以将非核心业务代码将业务层抽离

3、相关概念

  • 切面(aspect):横切面对象,一般为一个具体类对象(可以借助@Aspect声明),可以理解为要植入的新的业务功能,这个功能交给某个类负责,这个类就是切面。
  • 切入点(pointcut):对连接点拦截内容的一种定义,在原有的哪些业务方法上扩展新的业务,可以将切入点理解为方法的集合,可以是1个类或某些类。
  • 连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的的方法,可以简单理解为切入点中的一个具体方法。
  • 通知(advice):拦截到连接点之后只要执行的方法,可以理解为一个业务中的扩展逻辑的若干步骤,先做什么(before),再做什么(afterReturn),最后做什么。
  • 目标对象(target):封装原业务逻辑的对象。
  • 代理对象(proxy):负责调用切面中的方法为目标对象植入新的功能。

4、通知注解

注解用途
@Pointcut定义切入点
@Before目标方法执行之前执行
@After目标方法执行之后必定执行,无论是否报错
@AfterReturning目标方法有返回值且正常返回后执行
@AfterThrowing目标方法抛出异常后执行
@Around可以获取到目标方法的入参和返回值

5、实现原理

  • 默认使用 Java 动态代理来创建 AOP 代理,这样就可以为任何接口实例创建代理了。
  • 当需要代理的类不是代理接口的时候,Spring 会切换为使用 CGLIB 代理。

6、Maven导包

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

7、定义切面

  1. 创建1个切面类。
  2. 在类上添加注解@Aspect@Component
  3. 如果有多个切面可以使用@Order注解指定顺序,指定的值越小越先执行。

8、定义切入点

通过execution表达式来给1个类或某些类定义切入点。

//com.macro.mall.tiny.controller包中所有类的public方法都应用切面里的通知
execution(public * com.macro.mall.tiny.controller.*.*(..))
//com.macro.mall.tiny.service包及其子包下所有类中的所有方法都应用切面里的通知
execution(* com.macro.mall.tiny.service..*.*(..))
//com.macro.mall.tiny.service.PmsBrandService类中的所有方法都应用切面里的通知
execution(* com.macro.mall.tiny.service.PmsBrandService.*(..))

列:

//切点(第一个*表示返回值,第二个*表示任意类,第三个*表示任意方法,(..)表示任意参数)
    @Pointcut("execution(* com.aop.service.*.*(..))")
    public void demo() {
    }

execution表达式解释

符号含义
第一个 /* 符号表示返回值的类型任意
com.aop.service即需要进行横切的业务包
包名后面的 ./*表示当前包下面的类
./*(…)表示任何方法名,括号表示参数,两个点表示任何参数类型

还可以和指定注解一起通过逻辑运算符(||、&&)来决定对连接点是否启用某个通知方法。

配合使用注解来启用某个通知方法

/* 后置通知 (目标方法执行之后必定执行,无论是否报错) 目标方法同时需要@NeedAspect注解的修饰,并且这里(通知)的形参名(annot)要与上面注解中的一致 */
@After("methods() && @annotation(annot)")
public void doAfter(NeedAspect annot) {
    log.info("=====After=====");
    log.debug("=====注解值[{}]=====", annot.value());
}
@RestController
@Slf4j
@RequestMapping("/aop")
public class AopController {

    @PostMapping("/add")
    @NeedAspect("add")
    public JSONObject add(@RequestBody JSONObject request) {
        log.info("=====请求报文=====\r\n{}", JSON.toJSONString(request, true));
        return success();
    }
}

还可以这样,同时监控多个

@Pointcut("execution(public * com.stuPayment.controller..*.*(..))")//切入点描述 这个是controller包的切入点
    public void controllerLog(){}//签名,可以理解成这个切入点的一个名称
    
    @Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入点描述,这个是uiController包的切入点
    public void uiControllerLog(){}
    //同时监控service 和 Controller
    @Before("controllerLog() || uiControllerLog()") //在切入点的方法run之前要干的
    public void logBeforeController(JoinPoint joinPoint) {
    //...
    }

8.JoinPoint 信息

public void before(JoinPoint joinPoint){
        System.out.println("前置通知");
        //获取目标方法的参数信息
        Object[] obj = joinPoint.getArgs();
        //AOP代理类的信息
        joinPoint.getThis();
        //代理的目标对象
        joinPoint.getTarget();
        //用的最多 通知的签名
        Signature signature = joinPoint.getSignature();
        //代理的是哪一个方法
        System.out.println("代理的是哪一个方法"+signature.getName());
        //AOP代理类的名字
        System.out.println("AOP代理类的名字"+signature.getDeclaringTypeName());
        //AOP代理类的类(class)信息
        signature.getDeclaringType();
        //获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //请求
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //会话
        HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
        //响应
        HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();
        //获取请求参数
        Enumeration<String> enumeration = request.getParameterNames();
        Map<String,String> parameterMap = new HashMap<>();
        while (enumeration.hasMoreElements()){
            String parameter = enumeration.nextElement();
            parameterMap.put(parameter,request.getParameter(parameter));
        }
        String str = JSON.toJSONString(parameterMap);
        if(obj.length > 0) {
            System.out.println("请求的参数信息为:"+str);
        }
    }

切入点表达式关键词

this:用于向通知方法中传入代理对象的引用

@Before(“before() && this(proxy)”)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}

target:用于向通知方法中传入目标对象的引用

@Before(“before() && target(target)”)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}

args:用于将方法的参数,传入到通知方法中

@Before(“before() && args(age,username)”)
public void beforeAdvide(JoinPoint point, int age, String username){
//处理逻辑
}

基础案例

pom.xml

<dependencies>
        <!-- 开发web 项目和启动Springboot必须添加的-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.4.1</version>
        </dependency>

<!-- 实现SpringBoot AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <!--spring-boot测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml

server:
  port: 10101

App

package com.aop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
}

com.aop.acpect(核心)

TestAop
package com.aop.acpect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(1)
public class TestAop {
        //切点(第一个*表示返回值,第二个*表示任意类,第三个*表示任意方法,(..)表示任意参数)
        @Pointcut("execution(* com.aop.service.*.*(..))")
        public void demo() {
        }

        @Pointcut("execution(* com.aop.controller.*.*(..))")
        public void demo1(){}

    //前置通知 (方法调用前被调用)
    @Before("demo()||demo1()")
    public void before(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        System.out.println("前置通知: "+name + "方法开始执行...");
    }

    //后置通知 (目标方法执行之后必定执行,无论是否报错)
    @After("demo()")
    public void after(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        System.out.println("后置通知: "+ name + "方法执行结束...");
    }

    //返回通知 (目标方法有返回值且正常返回后执行,报错则不执行,)
    @AfterReturning(value = "demo()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        String name = joinPoint.getSignature().getName();
        System.out.println("返回通知: "+name + "方法返回值为: " + result);
    }

    //后置异常通知 (只有目标方法抛出的异常或者方法相应参数异常类型时才能执行后置异常通知,如果方法正常就不执行, )
    @AfterThrowing(value = "demo()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        String name = joinPoint.getSignature().getName();
        System.out.println("后置异常通知: "+ name + "方法发生异常,异常是: " + e.getMessage());
    }

    /** * 环绕通知: 使用@Around就别使用单个的注解 * 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。 * 也就是可以获取到目标方法的入参和返回值,注意这里如果使用try catch将异常拦截了那么@AfterThrowing就会失效 */
// @Around("demo()")
// public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// System.out.println("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName());
// try {
// Object obj = proceedingJoinPoint.proceed();
// return obj;
// } catch (Throwable throwable) {
// throwable.printStackTrace();
// System.out.println("Around: 异常通知");
// }
// return null;
//
// }

}

com.aop.service

package com.aop.service;

import org.springframework.stereotype.Service;

@Service
public interface UserService {

    public String getUserById(Integer id);

    public void deleteUserById(Integer id);

}

com.aop.service.impl

UserServiceImpl
package com.aop.service.impl;

import com.aop.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Override
    public String getUserById(Integer id){
        System.out.println("UserServiceImpl: get...");
        return "User";
    }

    @Override
    public void deleteUserById(Integer id){
        int i=1/0;
        System.out.println("UserServiceImpl: delete...");
    }

}

com.aop.controller

UserController
package com.aop.controller;

import com.aop.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    // http://localhost:10101/getUserById?id=3
    @GetMapping("/getUserById")
    public String getUserById(Integer id){
        return userService.getUserById(id);
    }
    // http://localhost:10101/deleteUserById?id=3
    @GetMapping("/deleteUserById")
    public void deleteUserById(Integer id){
        userService.deleteUserById(id);
    }
}

自行访问UserController里的接口

案例: AOP接口访问日志

日志信息: 可以存在数据库中,也可以以文件的形式存储在本地,看需求,

如果日志需要进行分析或者根据日志进行开发东西的话那么建议存储在数据库中,

如果日志只是单纯的查看bug,那么我们可以存储在文件中(比如: log4j 或者 Logback)

下面我们就采用 logback日志 ,SpringBoot自带的,所以依赖什么的就不用引了!

注意: 在接口上使用swagger2的 @ApiOperation(value = “查询t_user表的全部数据”) 注解来描述接口,因为我们aop的条件就是获取接口上的这个注解里的信息

pom.xml

<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <dependencies>
        <!-- 开发web 项目和启动Springboot必须添加的-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.4.1</version>
        </dependency>

<!-- 实现SpringBoot AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--数据库驱动 告诉Springboot 我们使用mysql数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--jdbc的启动器,默认使用HikariCP连接池-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>

        <!--Mybati 和Spring boot 自动整合依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>


        <!--spring-boot测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--Swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

<!-- 工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.7</version>
        </dependency>

        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>javax.persistence-api</artifactId>
            <version>2.2</version>
        </dependency>
    </dependencies>
    <!-- 自动查找主类 用于打包 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

application.yml

server:
  port: 10101

spring:
  # 开发阶段关闭thymeleaf的模板缓存  否则每次修改都需要重启引导
  thymeleaf:
    cache: false
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/voidme?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false&serverTimezone=UTC&useSSL=false
    username: root
    password: root
    hikari.idle-timeout: 60000
    hikari.maximum-pool-size: 30
    hikari.minimum-idle: 10

#spring集成Mybatis环境
#实体类别名扫描包    如果使用接口的话这个可以省略
# 加载Mybatis映射文件 classpath:mapper/*Mapper.xml 代表是  resources 下mapper包里所有是 ..Mapper.xml结尾的xml配置文件  如果使用接口的话这个可以省略
mybatis:
  type-aliases-package: com.aop.pojo
  mapper-locations: classpath:mybaitis/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

logback.xml

文件名称必须是logback.xml,而且必须放在resources下面

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。 当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<!-- 总体说明:根节点下有2个属性,三个节点 属性: contextName 上下文名称; property 设置变量 节点: appender, root, logger -->
<configuration scan="true" scanPeriod="10 seconds">
    <!-- contextName说明: 每个logger都关联到logger上下文,默认上下文名称为“default”。但可以使用设置成其他名字, 用于区分不同应用程序的记录。一旦设置,不能修改,可以通过%contextName来打印日志上下文名称。 -->
    <contextName>logback-spring</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
    <property name="logging.path" value="myLogs"/>

    <!--0. 日志格式和颜色渲染 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--1. 输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>

        <!--日志文档输出格式-->
        <encoder>
            <!--指定日志格式-->
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!--设置字符集-->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--输出到文档-->
    <!-- 时间滚动输出 level为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名~~~~~file设置打印的文件的路径及文件名,建议绝对路径-->
        <file>${logging.path}/web_debug.log</file>

        <!--日志文档输出格式-->
        <encoder>
            <!--指定日志格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>

        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <!-- 日志记录器的滚动策略 SizeAndTimeBasedRollingPolicy 按日期,大小记录日志 另外一种方式: rollingPolicy的class设置为ch.qos.logback.core.rolling.TimeBasedRollingPolicy -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <!-- 归档的日志文件的路径,例如今天是2018-08-23日志,当前写的日志文件路径为file节点指定, 可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。 而2018-08-23的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
            <fileNamePattern>${logging.path}/web-debug/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>

            <!-- 配置日志文件不能超过100M,若超过100M,日志文件会以索引0开始,命名日志文件 例如error.20180823.0.txt -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>

            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>

        <!-- 此日志文档只记录debug级别的 -->
        <!-- 过滤策略: LevelFilter : 只打印level标签设置的日志级别 ThresholdFilter:打印大于等于level标签设置的级别,小的舍弃 -->
        <!--<filter class="ch.qos.logback.classic.filter.ThresholdFilter">-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的日志级别 -->
            <level>debug</level>
            <!--匹配到就允许-->
            <onMatch>ACCEPT</onMatch>
            <!--没有匹配到就禁止-->
            <onMismatch>DENY</onMismatch>
        </filter>

    </appender>

    <!-- level为 INFO 日志,时间滚动输出 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_info.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${logging.path}/web-info/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>1MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- level为 WARN 日志,时间滚动输出 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_warn.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logging.path}/web-warn/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- level为 ERROR 日志,时间滚动输出 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_error.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logging.path}/web-error/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- <logger>用来设置某一个包或者具体的某一个类的日志打印级别、 以及指定<appender>。<logger>仅有一个name属性, 一个可选的level和一个可选的addtivity属性。 name:用来指定受此logger约束的某一个包或者具体的某一个类。 level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF, 还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。 如果未设置此属性,那么当前logger将会继承上级的级别。 addtivity:是否向上级logger传递打印信息。默认是true。 <logger name="org.springframework.web" level="info"/> <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/> -->

    <!-- 使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作: 第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息 第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别: -->
    <!-- 配置自己dao层路径-->
    <logger name="com.apache.ibatis" level="DEBUG"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>
    <logger name="com.aop.dao" level="debug"/>

    <!-- root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性 level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF, 不能设置为INHERITED或者同义词NULL。默认是DEBUG 可以包含零个或多个元素,标识这个appender将会添加到这个logger。 -->

    <!-- root指定最基础的日志输出级别,level属性指定 appender-ref标识的appender将会添加到这个logger -->
    <root level="info">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="WARN_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>

    <!-- 生产环境:输出到文档-->
    <springProfile name="pro">
        <root level="info">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="DEBUG_FILE"/>
            <appender-ref ref="INFO_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
            <appender-ref ref="WARN_FILE"/>
        </root>
    </springProfile>

</configuration>

开发环境日志文件默认在项目里根目录,上线到linux里通过jar 来运行时候,日志文件默认位置和jar在一个文件夹下面

启动类

package com.aop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
}

com.aop.acpect

WebLogAspect
package com.aop.acpect;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.aop.entity.WebLog;
import io.swagger.annotations.ApiOperation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** * 统一日志处理切面 * Created by xc on 190903. */
@Aspect
@Component
@Order(1)
public class WebLogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    @Pointcut("execution(public * com.aop.controller.*.*(..))")
    public void webLog() {
    }


    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result =null;
        //记录请求信息
        WebLog webLog = new WebLog();
        long startTime = System.currentTimeMillis(); //开始时间
        try {

            webLog.setStartTime(DateUtil.now());//操作时间
            //获取当前请求对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            if (method.isAnnotationPresent(ApiOperation.class)) {
                ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
                webLog.setDescription(apiOperation.value());
            }

            webLog.setUsername("admin-测试用户"); //用户名到时候可以直接拿当前权限然后解析 比如: request.getHeader("jwttoken") 然后解析就行
            String urlStr = request.getRequestURL().toString();
            webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
            webLog.setIp(getIpAddr(request));
            webLog.setMethod(request.getMethod());
            webLog.setParameter(getParameter(method, joinPoint.getArgs()));
            webLog.setUri(request.getRequestURI());
            webLog.setUrl(request.getRequestURL().toString());

            result = joinPoint.proceed();//执行方法

            webLog.setResult(result); //记录方法返回值
            long endTime = System.currentTimeMillis();
            webLog.setSpendTime(  (endTime - startTime)+"ms" );//消耗时间
            webLog.setStatus(1);//记录是否操作成功

            //记录 -如果需要进行日志的分析等-到时将信息放入到数据库里就行了
            LOGGER.info("-------------------------------------\n{}", webLog);

        } catch (Throwable throwable) {
            webLog.setResult(result); //记录方法返回值
            long endTime = System.currentTimeMillis();
            webLog.setSpendTime( (endTime - startTime)+"ms");//消耗时间

            webLog.setStatus(0);//记录是否操作成功
            webLog.setMessage(throwable.getMessage());//打印报错信息
            //打印错误堆栈到日志中
            StringWriter errorsWriter = new StringWriter();
            throwable.printStackTrace(new PrintWriter(errorsWriter));
            webLog.setPrintStackTrace(errorsWriter.toString());
            LOGGER.error("-------------------------------------\n{}", webLog);
        }

        return result;
    }

    /** * 根据方法和传入的参数获取请求参数 */
    private Object getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            }
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }

    // 获取用户ip 注意: 0:0:0:0:0:0:0:1 也是本地
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    try {
                        ipAddress = InetAddress.getLocalHost().getHostAddress();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                }
            }
            // 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null) {
                if (ipAddress.contains(",")) {
                    return ipAddress.split(",")[0];
                } else {
                    return ipAddress;
                }
            } else {
                return "";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }
}

com.aop.controller

UserController
package com.aop.controller;

import com.aop.entity.UserDO;
import com.aop.service.UserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

//http://127.0.0.1:10101/user/usersOne?integer=1
    @ApiOperation("Aop日志测试")
    @GetMapping("/usersOne")
    public ResponseEntity<String> findOne(@RequestParam("integer") Integer integer){
        System.out.println(integer);
        return ResponseEntity.ok("AOP-test_log");
    }

    //http://127.0.0.1:10101/user/usersOneError?integer=1
    @ApiOperation("Aop日志报错测试")
    @GetMapping("/usersOneError")
    public ResponseEntity<String> findOneError(@RequestParam("integer") Integer integer){
        int i=1/0;
        System.out.println(integer);
        return ResponseEntity.ok("AOP-test_log");
    }


    //http://127.0.0.1:10101/user/selectUsers
    /*** * 查询t_user表的全部数据 * @return List<User> */
    @ApiOperation(value = "查询t_user表的全部数据")
    @GetMapping(value = "/selectUsers" )
    public ResponseEntity<List<UserDO>> selectUsers(){
        return ResponseEntity.ok(userService.selectUsers());
    }

}

com.aop.dao

UserDao
package com.aop.dao;

import com.aop.entity.UserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;
@Mapper
public interface UserDao {

    /*** * 查询t_user表的全部数据 * @return List<UserDO> */
    @Select("SELECT * FROM t_user ")
    List<UserDO> selectUsers();
}

com.aop.entity

UserDO
package com.aop.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;

/** * @Author huanmin */

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ApiModel(value = "UserDO",description = "")
@Table(name="t_user")
public class UserDO implements Serializable {


    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "userId")
    private String userId;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "name")
    private String name;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "pass")
    private String pass;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "age")
    private Integer age;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "sex")
    private String sex;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "address")
    private String address;
    @ApiModelProperty(value = "没有字段描述",required = false)
    @Column(name = "phone")
    private String phone;

}
WebLog
package com.aop.entity;

import lombok.Data;

/** * Controller层的日志封装类 */
@Data
public class WebLog {
    /** * 操作描述 */
    private String description;

    /** * 操作用户 */
    private String username;

    /** * 操作时间 */
    private String startTime;

    /** * 消耗时间 (单位毫秒: 1秒=1000毫秒) */
    private String spendTime;

    /** * 根路径 */
    private String basePath;

    /** * URI */
    private String uri;

    /** * URL */
    private String url;

    /** * 请求类型(方法名) */
    private String method;

    /** * IP地址 */
    private String ip;

    /** * 请求参数 */
    private Object parameter;

    /** * 请求返回的结果 */
    private Object result;

    /** * 访问操作是否成功: 0 未成功 1 成功 */
    private  int status;
    /** * 报错信息 */
    private String Message;

    /** * 报错堆栈信息 */
    private  String printStackTrace;

}

com.aop.service

UserService
package com.aop.service;

import com.aop.entity.UserDO;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public interface UserService {

    /*** * 查询t_user表的全部数据 * @return List<User> */
    List<UserDO> selectUsers();
}

com.aop.service.impl

UserServiceImpl
package com.aop.service.impl;

import com.aop.dao.UserDao;
import com.aop.entity.UserDO;
import com.aop.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    /*** * 查询t_user表的全部数据 * @return List<User> */
    @Override
    public List<UserDO> selectUsers(){
        return userDao.selectUsers();
    }
}

测试: 自行访问controller里的接口就行

案例:接口幂等性

场景:比如在一个支付场景 ,用户确认了订单,点击支付去付款,可能由于手误或者网络原因,前端向后端发起了两次请求支付的请求,如果接口没有幂等性的保证,那么用户会支付两次,这显然是不合理的。这种情况下我们有多种方式来保证接口的幂等性

方案1:我们的处理一般是给订单加一个支付状态,在付款之前会查询这个订单的支付状态,如果这个订单在之前的一次请求中已经支付,那么后面来的这个请求我们就不用去发起支付,直接返回用户支付成功即可,这样这个接口就可以说是有幂等性保证的。

方案2:就是我们在设计表的时候,有一张表来记录用户的支付记录,这个支付记录其中的一个字段是订单ID,我们可以把这个订单ID设置为唯一性的索引,如果用户对同一个订单发起了多交的支付,那么在插入这个表的时候必定会违反唯一性索引约束,这个时候我们也可以直接返回用户支付成功而不用真的去二次付款。

方案3: 使用token机制

用户请求后生成token将token放入到session中,执行完删除session内的token, 当用户多次请求就会去查询session中有没有,

如果有那么直接返回方法,然后提示用户,重复提交信息

方案4: 悲观锁 数据锁定时间可能会很长,根据实际情况选用

方案5: //乐观锁 // 乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。 (防止ABA问题,通过版本号方式解决)

方案6:使用redis或zookeeper分布式锁的方式 (redis推荐)

pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <dependencies>

<!-- redis 配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>
    <!-- 自动查找主类 用于打包 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

application.yml

server:
  port: 10101

spring:
  # redis配置
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1m

启动类

package com.aop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        return factory;
    }
}

com.aop.acpect

NoRepeatSubmit
package com.aop.acpect;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {

    /** * 设置请求锁定时间 默认10秒 * * @return */
    int lockTime() default 10;

}
RepeatSubmitAspect
package com.aop.acpect;

import com.aop.utils.ApiResult;
import com.aop.utils.RedisLock;
import com.aop.utils.RequestUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

@Aspect
@Component
public class RepeatSubmitAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);

    @Autowired
    private RedisLock redisLock;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointCut(NoRepeatSubmit noRepeatSubmit) {
    }

    @Around("pointCut(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        int lockSeconds = noRepeatSubmit.lockTime();

        HttpServletRequest request = RequestUtils.getRequest();
        Assert.notNull(request, "request can not null");

        // 此处可以用token或者JSessionId
        String token = request.getHeader("Authorization");
        String path = request.getServletPath();
        String key = getKey(token, path);
        String clientId = getClientId();
       //加锁
        boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
        LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
        if (isSuccess) {
            LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
            // 获取锁成功
            Object result;

            try {
                // 执行进程
                result = pjp.proceed();
            } finally {
                // 解锁
                redisLock.releaseLock(key, clientId);
                LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
            }

            return result;

        } else {
            // 获取锁失败,认为是重复提交的请求
            LOGGER.info("tryLock fail, key = [{}]", key);
            return new ApiResult(200, "重复请求,请稍后再试", null);
        }

    }

    private String getKey(String token, String path) {
        return token + path;
    }

    private String getClientId() {
        return UUID.randomUUID().toString();
    }

}

com.aop.controller

SubmitController
package com.aop.controller;

import com.aop.acpect.NoRepeatSubmit;
import com.aop.utils.ApiResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SubmitController {

    @PostMapping("submit")
    @NoRepeatSubmit(lockTime = 30)//设置接口锁,相同用户在规定的时间内只能访问一次
    public Object submit(@RequestBody UserBean userBean) {
        try {
            // 模拟业务场景
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return new ApiResult(200, "成功", userBean.userId);
    }

    public static class UserBean {
        private String userId;

        public String getUserId() {
            return userId;
        }

        public void setUserId(String userId) {
            this.userId = userId == null ? null : userId.trim();
        }
    }

}

com.aop.utils

ApiResult
package com.aop.utils;

public class ApiResult {

    private Integer code;

    private String message;

    private Object data;

    public ApiResult(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message == null ? null : message.trim();
    }

    public Object getData() {
        return data;
    }

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

    @Override
    public String toString() {
        return "ApiResult{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }
}
RedisLock
package com.aop.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;

import java.util.Collections;

/** * Redis 分布式锁实现 * */
@Service
public class RedisLock {

    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";

    private static final String SET_IF_NOT_EXIST = "NX";
    // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    // if get(key) == value return del(key)
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private StringRedisTemplate redisTemplate;

    /** * 该加锁方法仅针对单实例 Redis 可实现分布式加锁 对于 Redis 集群则无法使用 * 原因: * 如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master 宕机了, * Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。 * * * 支持重复,线程安全 * * @param lockKey 加锁键 * @param clientId 加锁客户端唯一标识(采用UUID) * @param seconds 锁过期时间 * @return */
    public boolean tryLock(String lockKey, String clientId, long seconds) {

        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            //集合成员是唯一的,这就意味着集合中不能出现重复的数据。 lockKey是用户+请求的路径 clientId是uuid随机生成的
            String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
            //如果添加成功代表 那么用户是第一次访问这个路径,那么就给他放行
            //如果添加不成功 那么用户是重复提交的
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        });
    }

    /** * 与 tryLock 相对应,用作释放锁 * * @param lockKey * @param clientId * @return */
    public boolean releaseLock(String lockKey, String clientId) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            //从redis 中删除锁 ,(使用lua脚本 在redis 使用脚本是线程安全的)
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
                    Collections.singletonList(clientId));
            //删除成功后,当前用户才能在次提交请求
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        });
    }
}
RequestUtils
package com.aop.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class RequestUtils {

    public static HttpServletRequest getRequest() {
        ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return ra.getRequest();
    }
}

com.aop.test

RunTest
package com.aop.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

@Component
public class RunTest implements ApplicationRunner {

    private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("执行多线程测试");
        String url="http://localhost:10101/submit";
        CountDownLatch countDownLatch = new CountDownLatch(1);

        System.out.println("创建10个线程");
        ExecutorService executorService = new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());

        for(int i=0; i<10; i++){
            String userId = "userId" + i;
            HttpEntity request = buildRequest(userId);
            executorService.submit(() -> {
                try {
                    countDownLatch.await();
                    System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());
                    ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
                    System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        countDownLatch.countDown();
    }

    private HttpEntity buildRequest(String userId) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization", "yourToken");
        Map<String, Object> body = new HashMap<>();
        body.put("userId", userId);
        return new HttpEntity<>(body, headers);
    }

}

测试方式:

  1. 启动本地redis
  2. 启动该程序->自动运行RunTest里的代码
  3. 看控制台的打印信息

注意事项:

该加锁方法仅针对单实例的Redis 进行可实现分布式加锁

对于 Redis 集群/哨兵,分片, 则无法使用,也不是说是无法使用,但是无法保证主节点宕机后数据丢失的问题,可能会存在多个用户拿到锁的情况

原因:
如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master 宕机了,
Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。

解决方案 : 使用redisson ,这个的话可以看我博客里有Reids分布式锁的解决方案

相关文章