aop实现
1,注解
注解用于接口方法、接口参数、和请求实体的属性上。
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;/**锁的过期时间,单位秒*/int lockTime() default 60;
}
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.Method;
import java.util.Objects;@Aspect
@Component
public class ApiLockAspect {/*** 接口数据锁* <p>* 1,只有方法上存在DataLock注解,没有key ,用 package+类名+方法名 组合成一个lock的key* 2,只有方法上存在DataLock注解,有key ,用key做lock的key* 3,若参数有注解,且注解是基本类型或String,Number,则用1,2,的key+参数做lock的key* 4,若参数是实体类有注解,则用1,2,的key+实体类带注解的属性 做lock的key*/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 {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();StringBuilder lockKey = new StringBuilder();// 获取方法上的DataLock注解的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,Number,Boolean包装类型,则只到这一层if (args[i].getClass().isPrimitive() || args[i] instanceof Number|| args[i] instanceof String || args[i] instanceof Boolean) {lockKey.append(SEPARATOR).append(args[i]);break;}// 实体类再遍历属性Field[] fields = args[i].getClass().getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(ApiLock.class)) {field.setAccessible(true);Object value = field.get(args[i]);if (!Objects.isNull(value)) {lockKey.append(SEPARATOR).append(value);}}}}}}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 {@ApiLockprivate Integer abc;private String recordNo;@ApiLockprivate String deviceSn;@ApiLockprivate String spaceNo;@ApiLockprivate int num ;@ApiLockprivate 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";@ApiOperation(value = "查询停车记录", notes = "查询停车记录")@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";}@ApiOperation(value = "查询停车记录-根据 id 查询", notes = "查询停车记录-根据 id 查询")@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 "发送成功,注意查收。";}}