使用场景:
1,防止数据保存连续点击,保存多条信息
2,防止多次点击,非幂等接口业务代码重复执行
3,短信验证码功能
4,接口流量锁
5,登录频率控制
aop实现
1,注解
注解用于接口方法、接口参数、和请求实体的属性上。
package com.navigation.sys.config.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author dll
*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLock {
/** lock的key前缀,不填时用会用方法路径作为key前缀*/
String prefix() default "";
/** 服务名,非必填,多服务时用*/
String serverName() default "";
/**加锁失败,提示信息*/
String msg() default "服务器忙,请等待片刻再操作。";
/**是否需要解锁,不解锁可实现类似校验发送短信验证码的功能*/
boolean unLock() default true;
/**默认锁的过期时间60秒 */
int timeOut() default 60000;
/**在实体类参数上加属性 */
String attrs() default "";
/** 锁的value值 默认等于1为锁模式,大于1为流量控制模式 */
int lockValue() default 1;
}
2,切面
import com.navigation.berth.common.util.LocalCache;
import com.navigation.common.core.exception.BizException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static java.util.stream.Collectors.toList;
@Aspect
@Component
public class ApiLockAspect {
/**
* 接口数据锁
* 1,方法上存在ApiLock注解,没有key参数,用 package+类名+方法名 组合成一个lock的key
* 2,方法上存在ApiLock注解,有key参数 ,用key做lock的key
* 3,若参数有注解,且注解是基本类型或String,Number,则用1,2,的key+参数做lock的key
* 4,若参数是实体类有注解且注解有attrs参数,则用1,2,的key+实体类attrs指定的属性 做lock的key
* 5,若参数是实体类有注解,则用1,2,的key+实体类带注解的属性 做lock的key
* 有 attrs 参数就不会在遍历属性,
*/
final static String SEPARATOR = ":";
@Around("@annotation(com.navigation.berth.aop.ApiLock)")
public Object dataLockAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String msg = method.getAnnotation(ApiLock.class).msg();
int lockTime = method.getAnnotation(ApiLock.class).lockTime();
boolean unlock = method.getAnnotation(ApiLock.class).unlock();
String lockKey = getLockKey(joinPoint);
System.out.println("lockKey = " + lockKey);
if (LocalCache.LOCK.containsKey(lockKey)) {
throw new BizException(msg);
}
// 加锁 多服务时改成redis锁,
try {
LocalCache.lock(lockKey, "1", lockTime * 1000L);
return joinPoint.proceed();
} catch (Throwable e) {
throw e;
} finally {
if (unlock) {
LocalCache.unLock(lockKey);
}
}
}
/**
* 关键点:如何生成lock的key
*/
private String getLockKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
StringBuilder lockKey = new StringBuilder();
// 获取方法上的ApiLock注解的key属性值
String serverName = method.getAnnotation(ApiLock.class).serverName();
String prefix = method.getAnnotation(ApiLock.class).prefix();
if (!serverName.trim().isEmpty()) {
lockKey.append(serverName).append(SEPARATOR);
}
if (!prefix.trim().isEmpty()) {
lockKey.append(prefix);
} else {
// 获取方法所在的类名和方法名
String className = signature.getDeclaringTypeName().replace(".", SEPARATOR);
String methodName = method.getName();
lockKey.append(className).append(SEPARATOR).append(methodName);
}
Object[] args = joinPoint.getArgs();
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
for (Annotation annotation : pa[i]) {
if (annotation instanceof ApiLock) {
// 参数是基本类型/包装类型/String,则只到这一层
if (args[i].getClass().isPrimitive() || args[i] instanceof String || args[i] instanceof Number
|| args[i] instanceof Boolean || args[i] instanceof Character || args[i] instanceof Byte) {
lockKey.append(SEPARATOR).append(args[i]);
break;
}
// 实体类参数有attrs属性值,则用指定的attrs属性值做key
String attrs = ((ApiLock) annotation).attrs();
boolean flag = !attrs.isEmpty();
if (flag) {
List<String> list = Arrays.stream(attrs.split(",")).map(String::trim).map(this::capitalizeFirstLetter).collect(toList());
for (String field : list) {
Method getMethod = args[i].getClass().getDeclaredMethod("get"+field);
getMethod.setAccessible(true);
Object result = getMethod.invoke(args[i]);
lockKey.append( SEPARATOR + result);
}
break;
}
// 实体类再遍历属性 fields 并不包含继承的属性
Field[] fields = args[i].getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ApiLock.class)) {
lockKey.append(getFieldValue(field, args[i]));
}
}
}
}
}
return lockKey.toString();
}
String getFieldValue(Field field, Object obj) throws IllegalAccessException {
field.setAccessible(true);
Object value = field.get(obj);
if (!Objects.isNull(value)) {
return SEPARATOR + value.toString();
}
return "";
}
// 将字符串的首字母转换为大写的方法
String capitalizeFirstLetter(String original) {
if (original == null || original.isEmpty()) {
return original;
}
return Character.toUpperCase(original.charAt(0)) + original.substring(1);
}
}
另一种方法getLockKey()
这种方法可以找到继承的属性,但多个一个依赖项,上面的方法没有其他依赖但不会查找父类的属性(不过也可以用反射遍历父类的方法查找属性解决clazz.getSuperclass())。
依赖了
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
/**
* 关键点:如何生成lock的key
*/
private String getLockKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InvocationTargetException, NoSuchFieldException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
StringBuilder lockKey = new StringBuilder();
// 获取方法上的ApiLock注解的key属性值
String serverName = method.getAnnotation(ApiLock.class).serverName();
String prefix = method.getAnnotation(ApiLock.class).prefix();
if (!serverName.trim().isEmpty()) {
lockKey.append(serverName).append(SEPARATOR);
}
if (!prefix.trim().isEmpty()) {
lockKey.append(prefix);
} else {
// 获取方法所在的类名和方法名
String className = signature.getDeclaringTypeName().replace(".", SEPARATOR);
String methodName = method.getName();
lockKey.append(className).append(SEPARATOR).append(methodName);
}
Object[] args = joinPoint.getArgs();
Annotation[][] pa = method.getParameterAnnotations();
// 遍历方法参数
for (int i = 0; i < args.length; i++) {
//遍历方法参数注解
for (Annotation annotation : pa[i]) {
if (annotation instanceof ApiLock) {
// 参数是基本类型/包装类型/String,则只到这一层
if (args[i].getClass().isPrimitive() || args[i] instanceof String || args[i] instanceof Number
|| args[i] instanceof Boolean || args[i] instanceof Character || args[i] instanceof Byte) {
lockKey.append(SEPARATOR).append(args[i]);
break;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(args[i].getClass());
// 实体类参数有attrs属性值,则用指定的attrs属性值做key
String attrs = ((ApiLock) annotation).attrs();
if (!attrs.isEmpty()) {
List<String> list = Arrays.stream(attrs.split(",")).collect(toList());
Map<String, PropertyDescriptor> propertyMap = Arrays.stream(targetPds).collect(Collectors.toMap(PropertyDescriptor::getName, pd -> pd));
for (String field : list) {
Method readMethod = propertyMap.get(field).getReadMethod();
readMethod.setAccessible(true);
Object obj = readMethod.invoke(args[i]);
lockKey.append(SEPARATOR).append(null == obj ? "" : obj);
}
break;
}
for (PropertyDescriptor pd : targetPds) {
// 如果属性仅由 getter 和 setter 方法定义,没有对应的字段,或者字段不在 getter 方法的声明类中,代码会抛出 NoSuchFieldException。
Field field = pd.getReadMethod().getDeclaringClass().getDeclaredField(pd.getName());
if (field.isAnnotationPresent(ApiLock.class)) {
field.setAccessible(true);
Object obj = field.get(args[i]);
lockKey.append(SEPARATOR).append(null == obj ? "" : obj);
}
}
}
}
}
return lockKey.toString();
}
}
3,hutool本地锁,可用redis锁替换
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author dll
*/
public class LocalCache {
// 超过一分钟缓存自动删除
public static final TimedCache<String, String> LOCK = CacheUtil.newTimedCache(1000*60);
static {
/** 每100ms检查一次过期 */
LOCK.schedulePrune(100);
}
public static void put(TimedCache<String, String> cache,String key, String value, Long timeout) {
/** 设置消逝时间 */
cache.put(key, value, timeout);
}
static Lock rtLock = new ReentrantLock();
public static boolean lock(String key, String value, Long timeout) {
if (rtLock.tryLock()) {
try {
if (LOCK.containsKey(key)) {
return false;
} else {
LocalCache.put(LOCK, key, "1", timeout);
return true;
}
} finally {
rtLock.unlock();
}
} else {
return false;
}
}
public static void unLock(String key) {
LOCK.remove(key);
}
}
自定义异常处理
1,自定义异常类
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
2,全局异常处理
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常处理
*/
@ExceptionHandler(BizException.class)
public Result handleBizException(HttpServletRequest request, BizException ex) {
log.error("请求地址URL: " + request.getRequestURL());
log.error(SeExceptionUtils.getStackTrace(ex), ex);
return Result.error(ex.getMessage());
}
}
测试
1,测试接口请求实体
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
/**
* @author dll
*/
@Data
@SuperBuilder
@NoArgsConstructor
@Accessors(chain = true)
public class MyQueryVO {
@ApiLock
private Integer abc;
private String recordNo;
@ApiLock
private String deviceSn;
@ApiLock
private String spaceNo;
@ApiLock
private int num ;
@ApiLock
private boolean bool ;
}
2,测试接口
import cn.hutool.core.thread.ThreadUtil;
import com.navigation.berth.aop.ApiLock;
import com.navigation.berth.aop.MyQueryVO;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("lock")
public class ApiLockTestController {
final static String prefix = "test:api:list";
@PostMapping("list/{id}/{b}")
@ApiLock(prefix = prefix,msg="正在处理...")
public String list(@PathVariable int id, @PathVariable @ApiLock boolean b, @RequestBody @ApiLock MyQueryVO vo) {
ThreadUtil.sleep(10000);
return "ok";
}
@GetMapping("{id}")
@ApiLock(serverName = "testSystem") // prefix没赋值会用方法路径赋值,prefix只在方法上有效 在参数和属性上没用
public String get(@PathVariable @ApiLock Long id) {
return "ok";
}
@ApiOperation(value = "发送验证码", notes = "60秒只发送一次")
@GetMapping("sendCode/{phone}")
@ApiLock(msg = "一分钟只能发送一次验证码",lockTime = 60,unlock = false)
public String sendCode(@PathVariable @ApiLock String phone) {
return "发送成功,注意查收。";
}
}
3,模拟连续操作
第一次操作
10秒内第二次操作
4,测试
流量控制
这里用的redis锁
1,redis配置
RedisConfig
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
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.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis缓存配置
*/
@Slf4j
@Configuration
public class RedisConfig {
public static final String redisTemplateBeanName = "redisTemplate";
@Bean(redisTemplateBeanName)
// @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer());
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer());
return redisTemplate;
}
// java8 时间
private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY);
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
private static ApplicationContext applicationContext;
protected RedisConfig(ApplicationContext context) {
RedisConfig.applicationContext = context;
}
public static <T> T getBean(Class<T> clazzType) {
return applicationContext.getBean(clazzType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
}
SeRedisUtils
import com.xxx.RedisConfig;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class SeRedisUtils {
public static RedisTemplate RT = RedisConfig.getBean(RedisConfig.redisTemplateBeanName,
RedisTemplate.class);
/**
* 设置指定 key 的值
* @param key
* @param value
*/
public static void set(String key, Object value) {
RT.opsForValue().set(key, value);
}
public static void set(String key, Object value, long timeout, TimeUnit unit) {
RT.opsForValue().set(key, value, timeout, unit);
}
public static Boolean delete(String key) {
return RT.delete(key);
}
/**
* 获取指定 key 的值
* @param key
* @return
*/
public static Object get(String key) {
return RT.opsForValue().get(key);
}
public static String getStr(String key) {
return (String) RT.opsForValue().get(key);
}
public static List<Object> executePipelined(SessionCallback<?> session) {
return RT.executePipelined(session);
}
public static List<Object> executePipelined(RedisCallback<?> action) {
return RT.executePipelined(action);
}
public static boolean lock (String key,Object value) {
return RT.opsForValue().setIfAbsent(key, value);
}
public static boolean lock(String key, Object value, long timeOut) {
return RT.opsForValue().setIfAbsent(key, value, timeOut, TimeUnit.SECONDS);
}
public static boolean lock (String key,Object value,long timeOut, TimeUnit unit) {
return RT.opsForValue().setIfAbsent(key, value, timeOut, unit);
}
public static void unLock (String key) {
delete(key);
}
}
2,ApiLockAspect.dataLockAdvice()
@Around("@annotation(com.navigation.sys.config.aop.ApiLock)")
public Object dataLockAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String msg = method.getAnnotation(ApiLock.class).msg();
int timeOut = method.getAnnotation(ApiLock.class).timeOut();
boolean unLock = method.getAnnotation(ApiLock.class).unLock();
int lockValue = method.getAnnotation(ApiLock.class).lockValue();
String lockKey = getLockKey(joinPoint);
log.info("lockKey = {}", lockKey);
if (lockValue == 1) {
if (!SeRedisUtils.lock(lockKey, lockValue, timeOut, TimeUnit.MILLISECONDS)) {
// 加锁失败
throw new BizException(msg);
}
// 加锁成功
try {
return joinPoint.proceed();
} finally {
if (unLock) {
SeRedisUtils.unLock(lockKey);
}
}
} else if (lockValue > 1) {
Object count = SeRedisUtils.get(lockKey);
if (count == null) {
// 第一次加锁 ;
if (SeRedisUtils.lock(lockKey, lockValue, timeOut, TimeUnit.MILLISECONDS)) {
return joinPoint.proceed();
}
} else if ((int) count > 0) {
// 锁已存在,value > 0, 减一操作;
if (SeRedisUtils.RT.opsForValue().decrement(lockKey, 1L) > 0L) {
return joinPoint.proceed();
}
throw new BizException(msg);
} else {
throw new BizException(msg);
}
}
return null;
}
3,测试
接口
@Slf4j
@RestController
@RequestMapping("test")
public class ApiLockTestController {
@Autowired
ApplicationContext context;
@GetMapping("testLockValue/{phone}/{password}")
public String testLockValue(@PathVariable String phone, String password) {
LocalDate now = LocalDateTimeUtil.now().toLocalDate();
log.info("当前日期:{}", now);
// 为了让spring注解生效 serviceLogin方法可以放到其他类里边,就不要从容器里边获调用方法了
context.getBean(ApiLockTestController.class).serviceLogin(phone, password, now.toString());
return "登录成功";
}
@ApiLock(prefix = "testLockValue", timeOut = 1000 * 3600 * 24, msg = "一天只能登录2次!", lockValue = 2, attrs = "phone,date")
public void serviceLogin( String phone, String password, String date) {
}
}