首页
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
Search
1
ElasticSearch ES 安装 Kibana安装 设置密码
421 阅读
2
记一个报错GC overhead limit exceeded解决方法
344 阅读
3
Teamcity + Rancher + 阿里云Code 实现Devops 自动化部署
230 阅读
4
JAVA秒杀系统的简单实现(Redis+RabbitMQ)
209 阅读
5
分布式锁Redisson,完美解决高并发问题
206 阅读
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
登录
/
注册
Search
标签搜索
AOP
支付
小说
docker
SpringBoot
XML
秒杀
K8S
RabbitMQ
工具类
Shiro
多线程
分布式锁
Redisson
接口防刷
Jenkins
Lewis
累计撰写
146
篇文章
累计收到
14
条评论
首页
栏目
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
页面
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
搜索到
5
篇与
的结果
2021-10-26
SpringBoot应用中使用AOP实现自定义登录注解及登录信息注入
{card-describe title="前言"}HandlerInterceptor经常被用来解决拦截事件,如用户鉴权等。另外,Spring也向我们提供了多种解析器Resolver,如用来统一处理异常的HandlerExceptionResolver,以及今天的主角HandlerMethodArgumentResolver。HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:supportsParameter(满足某种要求,返回true,方可进入resolveArgument做参数处理)resolveArgument{/card-describe}{dotted startColor="#ff6c6c" endColor="#1989fa"/}第一步:首先我们需要在Shiro里面添加需要放开的接口第二步:创建@Login的自定义注解/** * app登录效验 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Login { }第三步:创建@LoginUser的自定义注解/** * 登录用户信息 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser { } 第四步:利用HandlerInterceptorAdapter这个适配器来实现自己的拦截器 主要作用是实现带有@Login注解的Controller强制要求登录,验证登陆后将当前用户数据注入到到request里,方便后续根据userId,获取用户信息。/** * 权限(Token)验证 */ @Component public class AuthorizationInterceptor extends HandlerInterceptorAdapter { @Autowired private JwtUtils jwtUtils; public static final String USER_KEY = "userId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Login annotation; if(handler instanceof HandlerMethod) { annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class); }else{ return true; } if(annotation == null){ return true; } //获取用户凭证 String token = request.getHeader(jwtUtils.getHeader()); if(StringUtils.isBlank(token)){ token = request.getParameter(jwtUtils.getHeader()); } //凭证为空 if(StringUtils.isBlank(token)){ throw new BdAdminException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value()); } Claims claims = jwtUtils.getClaimByToken(token); if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){ throw new BdAdminException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value()); } //设置userId到request里,后续根据userId,获取用户信息 request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject())); return true; } }第五步:自定义方法参数解析器实现有@LoginUser注解的方法参数,注入当前登录用户@Component public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Autowired private UserService userService; @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception { //获取用户ID Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST); if(object == null){ return null; } //获取用户信息 UserEntity user = userService.getById((Long)object); return user; } }第六步:通过WebMvcConfig注册我们添加的拦截器、方法参数解析器/** * MVC配置 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; @Autowired private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**"); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserHandlerMethodArgumentResolver); } } 第七步:具体示例/** * APP测试接口 */ @RestController @RequestMapping("/app") @Api("APP测试接口") public class AppTestController { @Login @GetMapping("userInfo") @ApiOperation("获取用户信息") public R userInfo(@LoginUser UserEntity user){ return R.ok().put("user", user); } @Login @GetMapping("userId") @ApiOperation("获取用户ID") public R userInfo(@RequestAttribute("userId") Integer userId){ return R.ok().put("userId", userId); } @GetMapping("notToken") @ApiOperation("忽略Token验证测试") public R notToken(){ return R.ok().put("msg", "无需token也能访问。。。"); } }
2021年10月26日
149 阅读
0 评论
0 点赞
2021-10-26
SpringBoot应用中使用AOP记录接口操作日志
第一步:在annotation包下创建一个@interface类型的Log/** * 自定义操作日志记录注解 * */ @Target({ ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 模块 */ public String title() default ""; /** * 功能 */ public BusinessType businessType() default BusinessType.OTHER; /** * 操作人类别 */ public OperatorType operatorType() default OperatorType.MANAGE; /** * 是否保存请求的参数 */ public boolean isSaveRequestData() default true; /** * 是否保存响应的参数 */ public boolean isSaveResponseData() default true; } 第二步:在aspectj包下面建立一个LogAspect/** * 操作日志记录处理 */ @Aspect @Component public class LogAspect { private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class); private static final String LOG_IGNORE_PROPERTIES = "createBy,createDate,updateBy,updateDate,version,updateIp,delFlag,remoteAddr,requestUri,method,userAgent"; // 配置织入点 @Pointcut("@annotation(com.aidex.common.annotation.Log)") public void logPointCut() { } @Around("@annotation(com.aidex.common.annotation.Log)") public Object loggingAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 定义返回对象、得到方法需要的参数 Object resultData = null; Object[] args = joinPoint.getArgs(); long takeUpTime = 0; resultData = joinPoint.proceed(args); long endTime = System.currentTimeMillis(); takeUpTime = endTime - startTime; handleLog(joinPoint, null, resultData, takeUpTime); return resultData; } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "logPointCut()", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Exception e) { handleLog(joinPoint, e, null, 0); } protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, long takeUpTime) { try { // 获得注解 Log controllerLog = getAnnotationLog(joinPoint); if (controllerLog == null) { return; } // 获取当前的用户 LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest()); // *========数据库日志=========*// SysOperLog operLog = new SysOperLog(); operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); operLog.setOperIp(ip); operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); if (loginUser != null) { operLog.setOperName(loginUser.getUsername()); } if (e != null) { operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); operLog.setTakeUpTime(takeUpTime); //设置数据变更 setLogContent(operLog); // 保存数据库 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); } catch (Exception exp) { // 记录本地异常日志 LOG.error("==前置通知异常=="); LOG.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } } private void setLogContent(SysOperLog operLog) { if (ContextHandler.get(BaseEntity.LOG_TYPE) != null) { String logType = (String) ContextHandler.get(BaseEntity.LOG_TYPE); if (StringUtils.isNotBlank(logType)) { if ("insert".equals(logType)) { // 异步保存日志 } else if ("update".equals(logType)) { } else if ("delete".equals(logType)) { } } } } /** * 获取注解中对方法的描述信息 用于Controller层注解 * * @param log 日志 * @param operLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception { // 设置action动作 operLog.setBusinessType(log.businessType().ordinal()); // 设置标题 operLog.setTitle(log.title()); // 设置操作人类别 operLog.setOperatorType(log.operatorType().ordinal()); // 是否需要保存request,参数和值 if (log.isSaveRequestData()) { // 获取参数的信息,传入到数据库中。 setRequestValue(joinPoint, operLog); } // 是否需要保存response,参数和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数,放到log中 * * @param operLog 操作日志 * @throws Exception 异常 */ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception { String requestMethod = operLog.getRequestMethod(); if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { String params = argsArrayToString(joinPoint.getArgs()); operLog.setOperParam(StringUtils.substring(params, 0, 2000)); } else { Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000)); } } /** * 是否存在注解,如果存在就获取 */ private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method != null) { return method.getAnnotation(Log.class); } return null; } /** * 参数拼装 */ private String argsArrayToString(Object[] paramsArray) { String params = ""; if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (StringUtils.isNotNull(o) && !isFilterObject(o)) { try { Object jsonObj = JSON.toJSON(o); params += jsonObj.toString() + " "; } catch (Exception e) { } } } } return params.trim(); } /** * 判断是否需要过滤的对象。 * * @param o 对象信息。 * @return 如果是需要过滤的对象,则返回true;否则返回false。 */ @SuppressWarnings("rawtypes") public boolean isFilterObject(final Object o) { Class<?> clazz = o.getClass(); if (clazz.isArray()) { return clazz.getComponentType().isAssignableFrom(MultipartFile.class); } else if (Collection.class.isAssignableFrom(clazz)) { Collection collection = (Collection) o; for (Object value : collection) { return value instanceof MultipartFile; } } else if (Map.class.isAssignableFrom(clazz)) { Map map = (Map) o; for (Object value : map.entrySet()) { Map.Entry entry = (Map.Entry) value; return entry.getValue() instanceof MultipartFile; } } return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult; } }第三步:在Controller中使用自定义的注解记录操作@Log(title = "用户管理", businessType = BusinessType.EXPORT)
2021年10月26日
116 阅读
0 评论
0 点赞
2021-10-22
SpringBoot 如何进行限流?老鸟们都这么玩的!
今天来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算法,如何优雅的进行限流。为什么要进行限流?因为互联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。那为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。例如,12306购票系统,在面对高并发的情况下,就是采用了限流。在流量高峰期间经常会出现提示语;"当前排队人数较多,请稍后再试!"什么是限流?有哪些限流算法?限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。{dotted startColor="#ff6c6c" endColor="#1989fa"/}常见的限流算法有三种:1.计数器限流计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。2.漏桶算法漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。3.令牌桶算法令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。基于Guava工具类实现限流Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效,实现步骤如下:第一步:引入guava依赖包<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency>第二步:给接口加上限流逻辑@Slf4j @RestController @RequestMapping("/limit") public class LimitController { /** * 限流策略 :1秒钟2个请求 */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/test1") public String testLimiter() { //500毫秒内,没拿到令牌,就直接进入服务降级 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf)); return "当前排队人数较多,请稍后再试!"; } log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf)); return "请求成功"; } }以上用到了RateLimiter的2个核心方法:create()、tryAcquire(),以下为详细说明acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 falsetryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 falsetryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 falsetryAcquire(int permits, long timeout, TimeUnit unit) 同上第三步:体验效果通过访问测试地址:http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38 WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38 INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38从以上日志可以看出,1秒钟内只有2次成功,其他都失败降级了,说明我们已经成功给接口加上了限流功能。当然了,我们在实际开发中并不能直接这样用。至于原因嘛,你想呀,你每个接口都需要手动给其加上tryAcquire(),业务代码和限流代码混在一起,而且明显违背了DRY原则,代码冗余,重复劳动。代码评审时肯定会被老鸟们给嘲笑一番,啥破玩意儿!{dotted startColor="#ff6c6c" endColor="#1989fa"/}所以,我们这里需要想办法将其优化 - 借助自定义注解+AOP实现接口限流。基于AOP实现接口限流基于AOP的实现方式也非常简单,实现过程如下:第一步:加入AOP依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>第二步:自定义限流注解@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ double permitsPerSecond () ; /** * 获取令牌最大等待时间 */ long timeout(); /** * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }第三步:使用AOP切面拦截限流注解@Slf4j @Aspect @Component public class LimitAop { /** * 不同的接口,不同的流量控制 * map的key为 Limiter.key */ private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.jianzh5.blog.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key=limit.key(); RateLimiter rateLimiter = null; //验证缓存是否有命中key if (!limitMap.containsKey(key)) { // 创建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回异常提示 if (!acquire) { log.debug("令牌桶={},获取令牌失败",key); this.responseFail(limit.msg()); return null; } } return joinPoint.proceed(); } /** * 直接向前端抛出异常 * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg); WebUtils.writeJson(response,resultData); } }第四步:给需要限流的接口加上注解@Slf4j @RestController @RequestMapping("/limit") public class LimitController { @GetMapping("/test2") @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!") public String limit2() { log.info("令牌桶limit2获取令牌成功"); return "ok"; } @GetMapping("/test3") @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!") public String limit3() { log.info("令牌桶limit3获取令牌成功"); return "ok"; } }第五步:体验效果通过访问测试地址:http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:正常响应时:{"status":100,"message":"操作成功","data":"ok","timestamp":1632579377104}触发限流时:{"status":2001,"message":"系统繁忙,请稍后再试!","data":null,"timestamp":1632579332177}通过观察得之,基于自定义注解同样实现了接口限流的效果。小结一般在系统上线时我们通过对系统压测可以评估出系统的性能阀值,然后给接口加上合理的限流参数,防止出现大流量请求时直接压垮系统。今天我们介绍了几种常见的限流算法(重点关注令牌桶算法),基于Guava工具类实现了接口限流并利用AOP完成了对限流代码的优化。在完成优化后业务代码和限流代码解耦,开发人员只要一个注解,不用关心限流的实现逻辑,而且减少了代码冗余大大提高了代码可读性,代码评审时谁还敢再笑话你?
2021年10月22日
77 阅读
0 评论
0 点赞
2021-03-08
一个注解搞定 SpringBoot 接口防刷
说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考。技术要点:springboot的基本知识,redis基本操作。1.首先是写一个注解类:import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); boolean needLogin()default true; }2.接着就是在Interceptor拦截器中实现:import com.alibaba.fastjson.JSON; import com.example.demo.action.AccessLimit; import com.example.demo.redis.RedisService; import com.example.demo.result.CodeMsg; import com.example.demo.result.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.OutputStream; @Component public class FangshuaInterceptor extends HandlerInterceptorAdapter { @Autowired private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断请求是否属于方法的请求 if(handler instanceof HandlerMethod){ HandlerMethod hm = (HandlerMethod) handler; //获取方法中的注解,看是否有该注解 AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null){ return true; } int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean login = accessLimit.needLogin(); String key = request.getRequestURI(); //如果需要登录 if(login){ //获取登录的session进行判断 //..... key+=""+"1"; //这里假设用户是1,项目中是动态获取的userId } //从redis中获取用户访问的次数 AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak,key,Integer.class); if(count == null){ //第一次访问 redisService.set(ak,key,1); }else if(count < maxCount){ //加1 redisService.incr(ak,key); }else{ //超出访问次数 render(response,CodeMsg.ACCESS_LIMIT_REACHED); //这里的CodeMsg是一个返回参数 return false; } } return true; } private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } }3.再把Interceptor注册到springboot中import com.example.demo.ExceptionHander.FangshuaInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Autowired private FangshuaInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor); } }4.接着在Controller中加入注解import com.example.demo.result.Result; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class FangshuaController { @AccessLimit(seconds=5, maxCount=5, needLogin=true) @RequestMapping("/fangshua") @ResponseBody public Result<String> fangshua(){ return Result.success("请求成功"); } }
2021年03月08日
92 阅读
0 评论
0 点赞
2021-03-08
SpringBoot--防止重复提交(分布式锁实现)
防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以redis分布式锁为例。1.自定义分布式锁注解import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheLock { //redis锁前缀 String prefix() default ""; //redis锁过期时间 int expire() default 5; //redis锁过期时间单位 TimeUnit timeUnit() default TimeUnit.SECONDS; //redis key分隔符 String delimiter() default ":"; }2.自定义key规则注解由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。import java.lang.annotation.*; @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheParam { String name() default ""; }3.定义key生成策略接口import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.stereotype.Service; public interface CacheKeyGenerator { //获取AOP参数,生成指定缓存Key String getLockKey(ProceedingJoinPoint joinPoint); }4.定义key生成策略实现类package com.example.demo.service.impl; import com.example.demo.service.CacheKeyGenerator; import com.example.demo.utils.CacheLock; import com.example.demo.utils.CacheParam; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class CacheKeyGeneratorImp implements CacheKeyGenerator { @Override public String getLockKey(ProceedingJoinPoint joinPoint) { //获取连接点的方法签名对象 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //Method对象 Method method = methodSignature.getMethod(); //获取Method对象上的注解对象 CacheLock cacheLock = method.getAnnotation(CacheLock.class); //获取方法参数 final Object[] args = joinPoint.getArgs(); //获取Method对象上所有的注解 final Parameter[] parameters = method.getParameters(); StringBuilder sb = new StringBuilder(); for(int i=0;i<parameters.length;i++){ final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class); //如果属性不是CacheParam注解,则不处理 if(cacheParams == null){ continue; } //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(cacheLock.delimiter()).append(args[i]); } //如果方法上没有加CacheParam注解 if(StringUtils.isEmpty(sb.toString())){ //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组) final Annotation[][] parameterAnnotations = method.getParameterAnnotations(); //循环注解 for(int i=0;i<parameterAnnotations.length;i++){ final Object object = args[i]; //获取注解类中所有的属性字段 final Field[] fields = object.getClass().getDeclaredFields(); for(Field field : fields){ //判断字段上是否有CacheParam注解 final CacheParam annotation = field.getAnnotation(CacheParam.class); //如果没有,跳过 if(annotation ==null){ continue; } //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量) field.setAccessible(true); //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object)); } } } //返回指定前缀的key return cacheLock.prefix() + sb.toString(); } }5.分布式注解实现import com.example.demo.service.CacheKeyGenerator; 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.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; @Aspect @Configuration public class CacheLockMethodInterceptor { @Autowired public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){ this.cacheKeyGenerator = cacheKeyGenerator; this.stringRedisTemplate = stringRedisTemplate; } private final StringRedisTemplate stringRedisTemplate; private final CacheKeyGenerator cacheKeyGenerator; @Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)") public Object interceptor(ProceedingJoinPoint joinPoint){ MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); CacheLock cacheLock = method.getAnnotation(CacheLock.class); if(StringUtils.isEmpty(cacheLock.prefix())){ throw new RuntimeException("前缀不能为空"); } //获取自定义key final String lockkey = cacheKeyGenerator.getLockKey(joinPoint); final Boolean success = stringRedisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit()) , RedisStringCommands.SetOption.SET_IF_ABSENT)); if (!success) { // TODO 按理来说 我们应该抛出一个自定义的 CacheLockException 异常;这里偷下懒 throw new RuntimeException("请勿重复请求"); } try { return joinPoint.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系统异常"); } } }6.主函数调整 @Bean public CacheKeyGenerator cacheKeyGenerator(){ return new CacheKeyGeneratorImp(); }7.Controller@ResponseBody @PostMapping(value ="/cacheLock") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock() public String cacheLock(String token){ return "sucess====="+token; } @ResponseBody @PostMapping(value ="/cacheLock1") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock(prefix = "redisLock.test",expire = 20) public String cacheLock1(String token){ return "sucess====="+token; } @ResponseBody @PostMapping(value ="/cacheLock2") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock(prefix = "redisLock.test",expire = 20) public String cacheLock2(@CacheParam(name = "token") String token){ return "sucess====="+token; }
2021年03月08日
23 阅读
0 评论
0 点赞