前言

  项目中经常会出现重复提交的问题,而接口幂等性也一直以来是做任何项目都要关注的疑难点,网上可以查到非常多的方案,我归纳了几点如下:



  1)、数据库层面,对责任字段设置唯一索引,这是最直接有效的方式,不好的地方就是一旦触发就会在服务端抛数据库相关异常;

  2)、代码层面,增加业务逻辑判断,先查询一遍若没有才插入,这也是最容易想到的方式,反正写上就对了,不好的地方就是分布式场景下依然避免不了问题;

  3)、前端层面,对于触发事件的操作比如按钮等,最好点击过后都设置几秒的置灰时间,能很大程度上解决恶意提交的问题。

  以上几点经常在项目中结合使用,不过有一种更通用的方案,就是自定义注解,写一个专门处理这类问题的注解,之后在有需要用到的接口上直接加上这个注解即可,十分方便。

项目

1、介绍

  本人所在公司是互联网行业,所以平常相当繁忙,但依然在这几天晚上得空之余咬牙爆肝做好了一个案例,网上有非常多相关的文章我也看过,但有些案例过于臃肿,我认为一个好的案例一定是轻巧、精简、清晰、一看就懂的迷你项目,我便是朝着这个方向来做的。

  通过这个小项目或者叫小案例,你可以学到这些技术:

  1)、SpringBoot2.6版本整合SpringDataRedis;

  2)、SpringBoot2.6版本整合Redisson;

  3)、SpringBoot2.6版本整合MybatisPlus最新版;

  4)、学会自定义一个注解;

  5)、学会防重复提交注解的核心实现并可灵活设置延迟时间。

2、源码

评论中分享

实现步骤

1、引入依赖

完整依赖如下:

使用的SpringBoot版本3.0之前最新的发布版本2.6.3,3.0版本要求JDK17,所以短时间内不会在行业中普遍的,企业中成熟的版本依然是2.x。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>resubmit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>resubmit</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring aop 实现自定义注解用到 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring processor 加载项目配置使用,也可以不用,但IDEA配置类顶端会有警告 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- spring jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql 不填写版本号默认是8.0以上 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- springData redis 不填写版本号默认和springboot版本一致 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redission 注意版本号和springData-redis要对应 这里26对应springData-redis的2.6版本 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-26</artifactId>
<version>3.16.8</version>
</dependency>
<!-- fastjson 解析json用到,也可以换成自己喜欢用的 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<!-- mybatis-plus 参考官网,目前是最新版 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 代码生成器 mybatisPlus自带的生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<!-- freemarker模板生成器 引入代码生成器需要 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<!-- swagger 因为mybatisPlus代码生成器会自带swagger的注解 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!-- lombok 因为mybatisPlus代码生成器会自带lombok的注解 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build> </project>
2、配置application.yml

这里注意,生产环境项目是要区分application.yml、application-dev.yml、application-test.yml、application-prod.yml的,这里只是实现功能的小案例,就只写了这一个。

# 端口,改成自己的。
server:
port: 8888 # 数据源,使用hikari,现在一般项目都是默认的这个数据源了,更详细的配置可以百度。
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/resubmit_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root # 换成自己数据库的账号
password: 123456 # 换成自己数据库的密码
# redis配置,换成自己的。
redis:
database: 11
host: 192.168.1.197
port: 6379
password: 123456
jedis:
pool:
max-active: 1000
max-wait: -1ms
max-idle: 50
min-idle: 1 # redission配置,这里直接读取的redis变量.
redisson:
singleserverconfig:
address: "redis://${spring.redis.host}:${spring.redis.port}"
password: ${spring.redis.password}
database: ${spring.redis.database}
3、编写配置类

1)、redis配置类

package com.example.resubmit.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; /**
* <p>
* redis配置类
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Configuration
public class RedisConfig { @Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
} @Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

2)、Redisson配置类

package com.example.resubmit.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils; /**
* <p>
* redission配置类
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Configuration
@ConfigurationProperties(prefix = "redisson.singleserverconfig")
public class RedissonSpringDataConfig { private static final Logger log = LoggerFactory.getLogger(RedissonSpringDataConfig.class); private String address;
private int database;
private String password; @Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) {
return new RedissonConnectionFactory(redisson);
} @Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws JsonProcessingException {
log.debug("[RedissonSpringDataConfig][redisson]>>>> address: {}, database: {}, password: {}", address, database, password);
Config config = new Config();
SingleServerConfig sconfig= config.useSingleServer()
.setAddress(address)
.setDatabase(database);
// 如果redis设置了密码,这里不设置密码就会报“org.redisson.client.RedisAuthRequiredException: NOAUTH Authentication required”错误。
if(StringUtils.hasText(password)){
sconfig.setPassword(password);
}
return Redisson.create(config);
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
} public int getDatabase() {
return database;
} public void setDatabase(int database) {
this.database = database;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
}
}
4、MybatisPlus代码生成器

这个生成器我参考官网的做了优化和注释,一看就能明白。

package com.example.resubmit.generator;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.Collections; /**
* @作者: 福隆苑居士,公众号:【9i分享客栈】
* @日期: 2022/2/8 20:51
* @描述: mybatis-plus代码生成器
*/
public class CodeGenerator { private static final String url = "jdbc:mysql://localhost:3306/resubmit_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=false";
private static final String username = "root";
private static final String password = "123456";
private static final String outputDir = "D:\workspace\workspace_java\resubmit\src\main\java"; // entity、mapper、service、controller生成的目录地址,换成自己项目的。
private static final String xmlOutputDir = "D:\workspace\workspace_java\resubmit\src\main\resources\mapper"; // xxMapper.xml生成的目录地址,换成自己项目的。 public static void main(String[] args) {
FastAutoGenerator.create(url, username, password)
.globalConfig(builder -> {
builder.author("福隆苑居士,公众号:【9i分享客栈】") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir(outputDir); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.example.resubmit") // 设置父包名,和自己项目的父包名一致即可。
.moduleName("") // 设置父包模块名,为空就会直接生成在父包名目录下。
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, xmlOutputDir)); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("tb_user") // 设置需要生成的表名,多个用逗号隔开。
.addTablePrefix("t_", "tb_", "c_"); // 设置过滤表前缀,多个用逗号隔开。
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
} }
5、定义响应实体

这个因人而定,可以自己定义,也可以直接用我的。

package com.example.resubmit.util;

import com.example.resubmit.enums.ResponseCodeEnum;
import com.fasterxml.jackson.annotation.JsonIgnore; /**
* <p>
* 自定义响应结果
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
public class ResultEntity<T> { private String code; private String msg; private T data; public ResultEntity(){} public ResultEntity(String code, String msg){
this.code = code;
this.msg = msg;
} public ResultEntity(String code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
} @JsonIgnore
public boolean isSuccess() {
return ResponseCodeEnum.SUCCESS.getCode().equals(this.getCode());
} public String getCode() {
return code;
} public void setCode(String code) {
this.code = code;
} public String getMsg() {
return msg;
} public void setMsg(String msg) {
this.msg = msg;
} public T getData() {
return data;
} public void setData(T data) {
this.data = data;
} public static ResultEntity fail(String code, String msg) {
return new ResultEntity(code, msg);
} public static <T> ResultEntity fail(String code, String msg, T data) {
return new ResultEntity(code, msg, data);
} public static ResultEntity ok(String code, String msg) {
return new ResultEntity(code, msg);
} public static <T> ResultEntity ok(String code, String msg, T data) {
return new ResultEntity(code, msg, data);
} public static ResultEntity ok(String msg) {
return new ResultEntity(ResponseCodeEnum.SUCCESS.getCode(), msg);
} public static <T> ResultEntity ok(String msg, T data) {
return new ResultEntity(ResponseCodeEnum.SUCCESS.getCode(), msg, data);
} public static ResultEntity fail(String msg) {
return new ResultEntity(ResponseCodeEnum.FAIL.getCode(), msg);
}
}
6、Redisson工具类
package com.example.resubmit.redission;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import javax.annotation.Resource;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; /**
* <p>
* 加锁解锁工具类
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Component
public class RedisLock {
private static final Logger log = LoggerFactory.getLogger(RedisLock.class); // todo 待优化,最好使用自定义的线程池,自定义工作队列和最大线程数。
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(4);
@Resource
private Redisson redisson; /**
* Redission获取锁
*
* @param lockKey 锁名
* @param uuid 唯一标识
* @param delaySeconds 过期时间
* @param unit 单位
* @return 是否获取成功
*/
public boolean Rlock(String lockKey, final String uuid, long delaySeconds, final TimeUnit unit) {
RLock rLock = redisson.getLock(lockKey);
boolean success = false;
try {
// log.debug("===lock thread id is :{}", Thread.currentThread().getId());
success = rLock.tryLock(0, delaySeconds, unit);
} catch (InterruptedException e) {
log.error("[RedisLock][Rlock]>>>> 加锁异常: ", e);
}
return success;
} /**
* Redission释放锁
*
* @param lockKey 锁名
*/
public void Runlock(String lockKey) {
RLock rLock = redisson.getLock(lockKey);
log.debug("[RedisLock][Rlock]>>>> {}, status: {} === unlock thread id is: {}", rLock.isHeldByCurrentThread(), rLock.isLocked(),
Thread.currentThread().getId());
rLock.unlock();
} /**
* Redission延迟释放锁
*
* @param lockKey 锁名
* @param delayTime 延迟时间
* @param unit 单位
*/
public void delayUnlock(final String lockKey, long delayTime, TimeUnit unit) {
if (!StringUtils.hasText(lockKey)) {
return;
}
if (delayTime <= 0) {
Runlock(lockKey);
} else {
EXECUTOR_SERVICE.schedule(() -> Runlock(lockKey), delayTime, unit);
}
} }
7、自定义注解

这里增加了延迟时间的属性,默认8秒。

package com.example.resubmit.redission;

import java.lang.annotation.*;

/**
* <p>
* 防重复提交注解
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotResubmit { /**
* 延时时间 在延时多久后可以再次提交,默认8秒
* @return 秒
*/
int delaySeconds() default 8;
}
8、防重注解的实现

这里是实现防重注解的核心代码,这里append实体属性时用到了toString()方法,所以要求我们生成的实体对象一定要有toString()方法。

package com.example.resubmit.redission;

import com.example.resubmit.enums.ResponseCodeEnum;
import com.example.resubmit.util.ResultEntity;
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.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils; import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit; /**
* <p>
* 防重复提交注解的实现,使用AOP。
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Aspect
@Component
public class LockMethodAOP {
private static final Logger log = LoggerFactory.getLogger(LockMethodAOP.class); @Resource
private RedisLock redisLock; /**
* 这里注意,我的注解写在同一个包下所以没有包名,如果换自己的目录,要改为@annotation(com.xxx.NotResubmit)加上完整包名.
*/
@Around("execution(public * *(..)) && @annotation(NotResubmit)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
// 获取到这个注解
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
NotResubmit lock = method.getAnnotation(NotResubmit.class); final String lockKey = generateKey(pjp); // 上锁
final boolean success = redisLock.Rlock(lockKey, null, lock.delaySeconds(), TimeUnit.SECONDS);
if (!success) {
// 这里也可以改为自己项目自定义的异常抛出
return ResponseEntity.badRequest().body(ResultEntity.fail(ResponseCodeEnum.FAIL.getCode(), "操作太频繁"));
}
return pjp.proceed();
} private String generateKey(ProceedingJoinPoint pjp) {
StringBuilder sb = new StringBuilder();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
sb.append(pjp.getTarget().getClass().getName())//类名
.append(method.getName());//方法名
for (Object o : pjp.getArgs()) {
sb.append(o.toString());
}
return DigestUtils.md5DigestAsHex(sb.toString().getBytes(Charset.defaultCharset()));
} }
9、编写控制器

这里在插入方法上加了@NotResubmit(delaySeconds = 10)注解,表示这个插入方法执行时,10秒内不允许重复提交,10秒后才可以插入成功,这个时间可以根据需要在不同的接口上自行修改。

package com.example.resubmit.controller;

import com.example.resubmit.entity.User;
import com.example.resubmit.redission.NotResubmit;
import com.example.resubmit.service.IUserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.stereotype.Controller; import java.time.LocalDateTime;
import java.util.List; /**
* <p>
* 控制器
* </p>
*
* @author 福隆苑居士,公众号:【9i分享客栈】
* @since 2022-02-08
*/
@Controller
@RequestMapping("/api/user")
public class UserController { private final IUserService userService; public UserController(IUserService userService) {
this.userService = userService;
} /**
* 查询列表
* @return 结果
*/
@GetMapping("/list")
public ResponseEntity<List<User>> list() {
return ResponseEntity.ok().body(userService.list());
} /**
* 插入记录
* @return 结果
*/
@NotResubmit(delaySeconds = 10)
@PostMapping("/insert")
public ResponseEntity<List<User>> insert(@RequestBody User user) {
// 插入
user.setCreatedAt(LocalDateTime.now());
user.setCreatedBy("冰敦敦");
user.setUpdatedAt(LocalDateTime.now());
user.setUpdatedBy("冰敦敦");
userService.save(user);
// 返回列表
return ResponseEntity.ok().body(userService.list());
}
}
10、效果

1)、执行接口插入一条记录

2)、连续点击看注解是否生效

可以发现,会返回操作太频繁的提示,并没有插入数据库。

3)、等待10秒过后再执行

发现又可以插入进去了

总结

  通过最终效果可以发现,自定义的防重注解实现起来并没有那么难,核心思想如下:



  把实体类的属性append并进行签名,作为redisson加锁的key,在aop拦截到使用这个注解的接口方法时,就会根据传入的对象和上一次提交时传入的对象进行属性签名的匹配,只要完全一致,代表是重复提交,只有在超过延迟时间后才能成功通过拦截,最终执行业务。



  这也是选取整合mybatisPlus的原因,因为它的代码生成器会自动生成带有toString()方法的代码,如果不想使用,直接用lombok注解也可以。

  最后,你其实可以发现,掌握了方法后,这个注解不仅可以拿来做防重,进行优化改造后还可以用来做接口限流,是不是很有意思呢。_

  以上内容是本人纯手打,如果觉得有一滴滴帮助,就麻烦伸出您的芊芊玉手点一下推荐吧!_

Springboot+Redisson自定义注解一次解决重复提交问题(含源码)的更多相关文章

  1. springboot aop 自定义注解方式实现完善日志记录(完整源码)

    版权声明:本文为博主原创文章,欢迎转载,转载请注明作者.原文超链接 一:功能简介 本文主要记录如何使用aop切面的方式来实现日志记录功能. 主要记录的信息有: 操作人,方法名,参数,运行时间,操作类型 ...

  2. iOS 自定义相机带拍摄区域边框及半透明遮罩层(含源码)

    开始时准备封装成ViewController的相机,但是在不改我相机控件源码的情况下,使用者很难自定义UI.于是想到将相机核心功能封装到View里,暴露出功能方法给外面调用,调用者只需将LFCamer ...

  3. Springboot 2.x 如何解决重复提交 (本地锁的实践)

    有没有遇到过这种情况:网页响应很慢,提交一次表单后发现没反应,然后你就疯狂点击提交按钮(12306就经常被这样怒怼),如果做过防重复提交还好,否则那是什么级别的灾难就不好说了... 本文主要是应用 自 ...

  4. 浅尝Spring注解开发_AOP原理及完整过程分析(源码)

    浅尝Spring注解开发_AOP原理及完整过程分析(源码) 浅尝Spring注解开发,基于Spring 4.3.12 分析AOP执行过程及源码,包含AOP注解使用.AOP原理.分析Annotation ...

  5. Springboot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)

    前言 我黄汉三又回来了,快半年没更新博客了,这半年来的经历实属不易,疫情当头,本人实习的公司没有跟员工共患难, 直接辞掉了很多人.作为一个实习生,本人也被无情开除了.所以本人又得重新准备找工作了. 算 ...

  6. SpringBoot:自定义注解实现后台接收Json参数

    0.需求 在实际的开发过程中,服务间调用一般使用Json传参的模式,SpringBoot项目无法使用@RequestParam接收Json传参 只有@RequestBody支持Json,但是每次为了一 ...

  7. springboot aop 自定义注解方式实现一套完善的日志记录(完整源码)

    https://www.cnblogs.com/wenjunwei/p/9639909.html https://blog.csdn.net/tyrant_800/article/details/78 ...

  8. springboot aop 自定义注解

    枚举类: /** * Created by tzq on 2018/5/21. */ public enum MyAnnoEnum { SELECT("select"," ...

  9. 基于springboot通过自定义注解和AOP实现权限验证

    一.移入依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spri ...

随机推荐

  1. xorm 条件查询时区的问题

    问题描述:如果在查询的时候,直接传时间格式作为条件,时间会被驱动程序转为UTC格式,因此会有8个小时的误差. 解决方案1: 将查询时间转为字符串 db.where("time > ?& ...

  2. python自动化测试框架的unittest与pytest前后置条件的区别

    前言: 笔者先试有用过unittest的前后置条件和pytest的前后置条件,觉得pytest的前后置条件比unittest的要简洁.方便很多.不过在使用unittest的前后置条件时,已经觉得在和每 ...

  3. python 设计模式:单例模型

    一.单例模型简介 代码的设计模式共有25种,不同的应用场景应用不同的设计模式,从而达到简化代码.利于扩展.提高性能等目的.本文简述Python实现的单例模式场景.简而言之,单例模式的应用场景是一个类对 ...

  4. etcd的raft选取机制

    etcd 是一个分布式的k/V存储系统.核心使用了RAFT分布式一致性协议.一致性这个概念,它是指多个服务器在状态达成一致,但是在一个分布式系统中,因为各种意外可能,有的服务器可能会崩溃或变得不可靠, ...

  5. Java日期格式化带来的年份不正确

    BUG现场 一个线上项目之前一直运行得很稳定,从没出过数据错误的问题,但是在2021.12.26这天却"意外"地出现了数据计算错误. 刚开始一头雾水,不知道是什么问题,后来经过日志 ...

  6. Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  7. 利用栈实现括号匹配(python语言)

    原理: 右括号总是与最近的左括号匹配 --- 栈的后进先出 从左往右遍历字符串,遇到左括号就入栈,遇到右括号时,就出栈一个元素与其配对 当栈为空时,遇到右括号,则此右括号无与之匹配的左括号 当最终右括 ...

  8. 【重构前端知识体系之HTML】讲讲对HTML5的一大特性——语义化的理解

    [重构前端知识体系之HTML]讲讲对HTML5的一大特性--语义化的理解 引言 在讲什么是语义化之前,先看看语义化的背景. 在之前的文章中提到HTML最重要的特性,那就是标签.但是项目一大,标签多的看 ...

  9. gorm连接mysql的初始化配置

    包含mysql配置.gorm配置.连接池配置.log日志配置 init_db_log.go文件代码 package main import ( "fmt" "gorm.i ...

  10. golang中goroutine

    1. 概念 goroutine 奉行通过通信来共享内存,而不是共享内存来通信 goroutine 是由go的运行时(runtime)调度和管理的 go程序会智能的将goroutine中的任务合理的分配 ...