diff --git a/java/spring/aop/aop模板/接口重复提交处理/重复提交处理.md b/java/spring/aop/aop模板/接口重复提交处理/重复提交处理.md new file mode 100644 index 0000000..1e011ba --- /dev/null +++ b/java/spring/aop/aop模板/接口重复提交处理/重复提交处理.md @@ -0,0 +1,90 @@ +# 接口重复提交处理 + +定义`AvoidRepeatableCommit`注解,用于对接口重复提交控制的切点 + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AvoidRepeatableCommit { + /** + * 指定时间内不可重复提交,单位毫秒,默认1秒 + */ + long timeout() default 1000; +} +``` + +定义**AOP**切面并实现接口重复提交控制逻辑 + +```java +@Component +@Aspect +@Slf4j +public class AvoidRepeatableCommitAspect { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + //设置切点为AvoidRepeatableCommit注解 + @Pointcut(value = "@annotation(avoidRepeatableCommit)") + public void pointcut(AvoidRepeatableCommit avoidRepeatableCommit) { + // pointcut + } + + //对切入点进行前置增强 + @Before(value = "pointcut(avoidRepeatableCommit)",argNames = "point,avoidRepeatableCommit") + public void before(JoinPoint point, AvoidRepeatableCommit avoidRepeatableCommit){ + + //获取请求对象 + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + + //获取注解 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + + //目标类、方法 + String className = method.getDeclaringClass().getName(); + String name = method.getName(); + + // 得到类名和方法 + String ipKey = String.format("%s#%s", className, name); + int hashCode = ipKey.hashCode(); + + //获取IP用于区分用户,这里的IP可以替换为用户ID或者Token + String ip = HttpRequestUtil.getIpAddr(request); + + String key = String.format("%s:%s_%d", "AVOID_REPEATABLE_COMMIT", ip, hashCode); + + log.info("ipKey={},hashCode={},key={}", ipKey, hashCode, key); + + long timeout = avoidRepeatableCommit.timeout(); + + String value = stringRedisTemplate.opsForValue().get(key); + + if (StringUtils.isNotBlank(value)) { + log.info("请勿重复提交表单"); + //抛出连续请求异常(全局异常处理会处理此异常) --> 请勿连续请求此接口 + throw new MyException(RestResponse.MSG.AVOID_REPEATABLE_COMMIT).setMsg("请勿连续请求此接口"); + } + + // 设置过期时间 + stringRedisTemplate.opsForValue().set(key,ipKey,timeout, TimeUnit.MILLISECONDS); + } +} + +``` + +将`AvoidRepeatableCommit`加到API方法上,可以实现用户对此API短时间内重复请求控制 + +```Java +@RestController +public class UserApi { + + //同一个用户在2000毫秒内重复调用此接口会抛出重复请求异常 + @AvoidRepeatableCommit(2000) + @GetMapping("/getList") + public String getList() { + return "test"; + } +} +``` + diff --git a/java/spring/aop/aop模板/接口防爬/设置API在规定时间内可调用次数.md b/java/spring/aop/aop模板/接口防爬/设置API在规定时间内可调用次数.md new file mode 100644 index 0000000..718386b --- /dev/null +++ b/java/spring/aop/aop模板/接口防爬/设置API在规定时间内可调用次数.md @@ -0,0 +1,171 @@ +# 设置API在规定时间内可调用次数 + +定义配置类,用于读取配置文件中的全局配置 + +```java +@RefreshScope //以这种方式注释的bean可以在运行时刷新,并且使用的任何组件 他们将在下一个方法调用中获得一个新实例,完全初始化并注入 与所有依赖关系。 +@ConfigurationProperties(prefix = "request-limit") +@Component +@Data +public class RequestLimitConfig { + /** + * 是否开启请求限制 + */ + private Boolean start; + + /** + * 允许访问的数量 + */ + private int amount; + + /** + * 时间段 + */ + private long time; +} + +``` + +定义RequestLimit注解,用于设置API在规定时间内允许访问的次数 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RequestLimit { + + /** + * 允许访问的次数,默认值100 + */ + int amount() default 100; + + /** + * 时间段,单位为毫秒,默认值一分钟 + */ + long time() default 60000; +} +``` + +切面实现 + +```java +@Aspect +@RefreshScope +@Component +@Slf4j +public class RequestLimitAspect { + + private final String POINT = "execution(* com.xxx.xxx.api..*.*(..))"; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private RequestLimitConfig requestLimitConfig; + + //设置切点为指定包下的方法 + @Pointcut(POINT) + public void pointcut() { + + } + + /** + * 对切点进行前置增强 + */ + @Before(value = "pointcut()",argNames = "point") + public void before(JoinPoint point) throws Throwable { + + // 判断是否开启了接口请求限制 (此处可以使用@Conditional优化,使用Conditional控制是否注入此切面) + if (requestLimitConfig.getStart()) { + ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + HttpServletRequest request = attribute.getRequest(); + + //获取IP + String ip = IpUtils.getIpAddr(request); + + //获取请求路径 + String url = request.getRequestURL().toString(); + + //获取方法名称 + String methodName = point.getSignature().getName(); + + String key = String.format("%s:%s:%s","REQUEST_LIMIT",ip,methodName); + Method currentMethod = getMethod(point); + + //查看接口是否有RequestLimit注解,如果没有则按yml的值全局验证 + if (currentMethod.isAnnotationPresent(RequestLimit.class)) { + //获取注解 + RequestLimit requestLimit = currentMethod.getAnnotation(RequestLimit.class); + boolean checkResult = checkWithRedis(requestLimit.amount(), requestLimit.time(), key); + if (checkResult) { + log.info("requestLimited," + "[用户ip:{}],[访问地址:{}]超过了限定的次数[{}]次", ip, url, requestLimit.amount()); + //抛出接口请求过于频繁异常(全局异常处理会处理此异常) --> 接口请求过于频繁 + throw new Exception(); + } + return; + } + boolean checkResult = checkWithRedis(requestLimitConfig.getAmount(), requestLimitConfig.getTime(), key); + if (checkResult) { + log.info("requestLimited," + "[用户ip:{}],[访问地址:{}]超过了限定的次数[{}]次", ip, url, requestLimitConfig.getAmount()); + //抛出接口请求过于频繁异常(全局异常处理会处理此异常) --> 接口请求过于频繁 + throw new Exception(); + } + } + } + + /** + * 以redis实现请求记录 + * @param amount 请求次数 规定时间内可请求的次数 + * @param time 规定时间 + * @param key redis key + * @return true 超过规定时间请求限制阈值 false 未超过规定时间请求限制阈值 + */ + private boolean checkWithRedis(int amount, long time, String key) { + //请求次数+1并取得已请求数 + long count = Optional.ofNullable(stringRedisTemplate.opsForValue().increment(key,1)).orElse(0L); + //如果是第一次请求则设置超时时间 + if (1==count) { + stringRedisTemplate.expire(key,time,TimeUnit.MILLISECONDS); + } + //判断已请求数是否超过规定时间内可请求次数 + return count > amount; + } + + /** + * 获取当前切点的方法 + */ + private Method getMethod(JoinPoint point) throws NoSuchMethodException { + Signature sig = point.getSignature(); + MethodSignature msig = (MethodSignature) sig; + Object target = point.getTarget(); + return target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); + } +} +``` + +配置文件设置全局api请求限制 + +```yaml +#请求限制参数 +request-limit: + start: false # 是否开启请求限制,默认关闭 //此处不开启单独使用注解不生效 + amount: 100 # 100次 + time: 60000 # 1分钟 单位毫秒 +``` + +单独对某个API设置请求限制 + +```java +@RestController +public class UserApi { + //设置接口来自于同一个IP的请求在1分钟内不能超过200次 + //amout 规定次数 time 规定时间 单位毫秒;如果配置文件中没有开启请求限制添加此注解不生效 + @RequestLimit(amount = 200, time = 60000) + @GetMapping("/getList") + public String getList() { + return "test"; + } +} +``` + diff --git a/java/spring/aop/aop模板/权限校验/权限校验.md b/java/spring/aop/aop模板/权限校验/权限校验.md new file mode 100644 index 0000000..98ce6b6 --- /dev/null +++ b/java/spring/aop/aop模板/权限校验/权限校验.md @@ -0,0 +1,99 @@ +# AOP权限校验实现 + +定义`AuthorityVerify`权限校验注解,用于权限校验的切点 + +```java +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthorityVerify { + String value() default ""; +} +``` + +定义**AOP**切面并实现针对权限校验的实现 + +```java +@Aspect +@Component +@Slf4j +public class AuthorityVerifyAspect { + + @Autowired + CategoryMenuService categoryMenuService; + + @Autowired + RedisUtil redisUtil; + + //设置切点为AuthorityVerify注解 + @Pointcut(value = "@annotation(authorityVerify)") + public void pointcut(AuthorityVerify authorityVerify) { + + } + + //对切点进行环绕增强 + @Around(value = "pointcut(authorityVerify)") + public Object doAround(ProceedingJoinPoint joinPoint, AuthorityVerify authorityVerify) throws Throwable { + + ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + HttpServletRequest request = attribute.getRequest(); + + //获取请求路径 + String url = request.getRequestURI(); + + // 解析出请求者的ID和用户名 + String adminUid = request.getAttribute(SysConf.ADMIN_UID).toString(); + + String key = String.format("%s:%s","ADMIN_VISIT_MENU",adminUid); + + // 从Redis中获取用户的权限信息 + String visitUrlStr = redisUtil.get(key); + + LinkedTreeMap visitMap = new LinkedTreeMap<>(); + + //判断用户权限是否存储再Redis中 + if (StringUtils.isNotEmpty(visitUrlStr)) { + // 存在则讲redis取到的字符串进行序列化 + visitMap = (LinkedTreeMap) JsonUtils.jsonToMap(visitUrlStr, String.class); + } else { + // Redis中不存在则从数据库中获取并存储到Redis中 + + // 查询数据库获取获取用户权限列表 + List buttonList = categoryMenuService.getMenuByUserId(adminUid); + + //将权限添加到Map中 + for (CategoryMenu item : buttonList) { + if (StringUtils.isNotEmpty(item.getUrl())) { + visitMap.put(item.getUrl(), item.getUrl()); + } + } + // 将访问URL存储到Redis中 + redisUtil.setEx(key, JsonUtils.objectToJson(visitMap), 1, TimeUnit.HOURS); + } + + // 判断该角色是否能够访问该接口 + if (visitMap.get(url) != null) { + log.info("用户拥有操作权限,访问的路径: {},拥有的权限接口:{}", url, visitMap.get(url)); + //执行业务 + return joinPoint.proceed(); + } else { + log.info("用户不具有操作权限,访问的路径: {}", url); + return ResultUtil.result(ECode.NO_OPERATION_AUTHORITY, MessageConf.RESTAPI_NO_PRIVILEGE); + } + } +} +``` + +将`AuthorityVerify`加到Api方法上则可以实现用户对此API的权限校验 + +```Java +@RestController +public class UserApi { + @AuthorityVerify + @GetMapping("/getList") + public String getList() { + return "test"; + } +} +``` + diff --git a/maven/Maven.md b/maven/Maven.md index ac51d3f..569a4a3 100644 --- a/maven/Maven.md +++ b/maven/Maven.md @@ -1,16 +1,15 @@ > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [javaguide.cn](https://javaguide.cn/tools/maven/maven-core-concepts.html) -> Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。 +# Maven -> 这部分内容主要根据 Maven 官方文档整理,做了对应的删减,主要保留比较重要的部分,不涉及实战,主要是一些重要概念的介绍。 +## 目录 -# Maven 介绍 ------------------------ +[TOC] + +## Maven 介绍 [Mavenopen in new window](https://github.com/apache/maven) 官方文档是这样介绍的 Maven 的: -> Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project's build, reporting and documentation from a central piece of information. -> > Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。 **什么是 POM?** 每一个 Maven 工程都有一个 `pom.xml` 文件,位于根目录中,包含项目构建生命周期的详细信息。通过 `pom.xml` 文件,我们可以定义项目的坐标、项目依赖、项目信息、插件信息等等配置。 @@ -23,7 +22,7 @@ 关于 Maven 的基本使用这里就不介绍了,建议看看官网的 5 分钟上手 Maven 的教程:[Maven in 5 Minutesopen in new window](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) 。 -[#](#maven-坐标) Maven 坐标 +Maven 坐标 ----------------------- 项目中依赖的第三方库以及插件可统称为构件。每一个构件都可以使用 Maven 坐标唯一标识,坐标元素包括: @@ -38,30 +37,28 @@ 举个例子(引入阿里巴巴开源的 EasyExcel) : -``` +```xml com.alibaba easyexcel 3.1.1 - - ``` 你可以在 https://mvnrepository.com/ 这个网站上找到几乎所有可用的构件,如果你的项目使用的是 Maven 作为构建工具,那这个网站你一定会经常接触。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/tools/maven/mvnrepository.com.png) +![](assets/mvnrepository.com.png) -[#](#maven-依赖) Maven 依赖 +Maven 依赖 ----------------------- 如果使用 Maven 构建产生的构件(例如 Jar 文件)被其他的项目引用,那么该构件就是其他项目的依赖。 -### [#](#依赖配置) 依赖配置 +### 依赖配置 **配置信息示例** : -``` +```xml @@ -80,8 +77,6 @@ - - ``` **配置说明** : @@ -94,7 +89,7 @@ * optional(可选): 标记依赖是否可选 * exclusions(可选):用来排除传递性依赖, 例如 jar 包冲突 -### [#](#依赖范围) 依赖范围 +### 依赖范围 **classpath** 用于指定 `.class` 文件存放的位置,类加载器会从该路径中加载所需的 `.class` 文件到内存中。 @@ -112,26 +107,23 @@ Maven 的依赖范围如下: * **runtime**:运行时依赖范围,对于测试和运行有效,但是在编译主代码时无效,典型的就是 JDBC 驱动实现。 * **system**:系统依赖范围,使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径,不依赖 Maven 仓库解析,所以可能会造成建构的不可移植。 -### [#](#传递依赖性) 传递依赖性 +### 传递依赖性 -### [#](#依赖冲突) 依赖冲突 +### 依赖冲突 **1、对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version。** -``` +```xml in.hocg.boot mybatis-plus-spring-boot-starter 1.0.48 - in.hocg.boot mybatis-plus-spring-boot-starter 1.0.49 - - ``` 若相同类型但版本不同的依赖存在于同一个 pom 文件,只会引入后一个声明的依赖。 @@ -143,8 +135,6 @@ Maven 的依赖范围如下: ``` 依赖链路一:A -> B -> C -> X(1.0) 依赖链路二:A -> D -> X(2.0) - - ``` 这两条依赖路径上有两个版本的 X,为了避免依赖重复,Maven 只会选择其中的一个进行解析。 @@ -169,8 +159,6 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** ``` 依赖链路一:A -> B -> X(1.0) // dist = 3 依赖链路二:A -> D -> X(2.0) // dist = 2 - - ``` 因此,Maven 又定义了声明顺序优先原则。 @@ -181,18 +169,16 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** 在依赖路径长度相等的前提下,在 `pom.xml` 中依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。该例中,如果 B 的依赖声明在 D 之前,那么 X (1.0) 就会被解析使用。 -``` +```xml ... dependency B ... dependency D - - ``` -### [#](#排除依赖) 排除依赖 +### 排除依赖 单纯依赖 Maven 来进行依赖调解,在很多情况下是不适用的,需要我们手动排除依赖。 @@ -201,8 +187,6 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** ``` 依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2 - - ``` 根据路径最短优先原则,X(1.0) 会被解析使用,也就是说实际用的是 1.0 版本的 X。 @@ -213,7 +197,7 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** **如何解决呢?** 我们可以通过`exclusive`标签手动将 X(1.0) 给排除。 -``` +```xml ...... @@ -223,8 +207,6 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** - - ``` 一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 jar 在升级的时候都会做到向下兼容。 @@ -236,13 +218,11 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** ``` 依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2 - - ``` 我们保留了 1.5 版本的 X,但是这个版本的 X 删除了 1.0 版本中的某些类。这个时候,我们可以考虑升级 D 的版本到一个 X 兼容的版本。 -[#](#maven-仓库) Maven 仓库 +Maven 仓库 ----------------------- 在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为 **构件** 。 @@ -266,7 +246,7 @@ Maven 依赖包寻找顺序: 2. 本地仓库没有找到的话,会去远程仓库找寻,下载包到本地仓库。 3. 远程仓库没有找到的话,会报错。 -[#](#maven-生命周期) Maven 生命周期 +Maven 生命周期 --------------------------- Maven 的生命周期就是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。 @@ -281,11 +261,11 @@ Maven 定义了 3 个生命周期`META-INF/plexus/components.xml`: 执行 Maven 生命周期的命令格式如下: -### [#](#default-生命周期) default 生命周期 +### default 生命周期 `default`生命周期是在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。 -``` +```xml validate @@ -334,27 +314,18 @@ Maven 定义了 3 个生命周期`META-INF/plexus/components.xml`: deploy - - ``` 根据前面提到的阶段间依赖关系理论,当我们执行 `mvn test`命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。 -### [#](#clean-生命周期) clean 生命周期 +### clean 生命周期 clean 生命周期的目的是清理项目,共包含 3 个阶段: -1. pre-clean -2. clean -3. post-clean - -``` +```xml - pre-clean - clean - post-clean @@ -362,24 +333,16 @@ clean 生命周期的目的是清理项目,共包含 3 个阶段: org.apache.maven.plugins:maven-clean-plugin:2.5:clean - - ``` 根据前面提到的阶段间依赖关系理论,当我们执行 `mvn clean` 的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。 -### [#](#site-生命周期) site 生命周期 +### site 生命周期 site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段: -1. pre-site -2. site -3. post-site -4. site-deploy - -``` +```xml - pre-site site @@ -396,24 +359,22 @@ site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段 org.apache.maven.plugins:maven-site-plugin:3.3:deploy - - ``` Maven 能够基于 `pom.xml` 所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。 -[#](#maven-插件) Maven 插件 +Maven 插件 ----------------------- Maven 本质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立完成的。像咱们日常使用到的 install、clean、deploy 等命令,其实底层都是一个一个的 Maven 插件。关于 Maven 的核心插件可以参考官方的这篇文档:https://maven.apache.org/plugins/index.html 。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/tools/maven/maven-plugins.png) +![](assets/maven-plugins.png) 除了 Maven 自带的插件之外,还有一些三方提供的插件比如单测覆盖率插件 jacoco-maven-plugin、帮助开发检测代码中不合规范的地方的插件 maven-checkstyle-plugin、分析代码质量的 sonar-maven-plugin。并且,我们还可以自定义插件来满足自己的需求。 jacoco-maven-plugin 使用示例: -``` +```xml @@ -435,10 +396,8 @@ jacoco-maven-plugin 使用示例: - - - - + + ``` 你可以将 Maven 插件理解为一组任务的集合,用户可以通过命令行直接运行指定插件的任务,也可以将插件任务挂载到构建生命周期,随着生命周期运行。 @@ -448,7 +407,7 @@ Maven 插件被分为下面两种类型: * **Build plugins** :在构建时执行。 * **Reporting plugins**:在网站生成过程中执行。 -[#](#maven-多模块管理) Maven 多模块管理 +Maven 多模块管理 ----------------------------- 多模块管理简单地来说就是将一个项目分为多个模块,每个模块只负责单一的功能实现。直观的表现就是一个 Maven 项目中不止有一个 `pom.xml` 文件,会在不同的目录中有多个 `pom.xml` 文件,进而实现多模块管理。 @@ -464,7 +423,7 @@ Maven 插件被分为下面两种类型: 如下图所示,Dubbo 项目就被分成了多个子模块比如 dubbo-common(公共逻辑模块)、dubbo-remoting(远程通讯模块)、dubbo-rpc(远程调用模块)。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/tools/maven/dubbo-maven-multi-module.png) +![](assets/dubbo-maven-multi-module.png) [#](#文章推荐) 文章推荐 --------------- diff --git a/maven/assets/dubbo-maven-multi-module.png b/maven/assets/dubbo-maven-multi-module.png new file mode 100644 index 0000000..f1f0cc1 Binary files /dev/null and b/maven/assets/dubbo-maven-multi-module.png differ diff --git a/maven/assets/maven-plugins.png b/maven/assets/maven-plugins.png new file mode 100644 index 0000000..f27486f Binary files /dev/null and b/maven/assets/maven-plugins.png differ diff --git a/maven/assets/mvnrepository.com.png b/maven/assets/mvnrepository.com.png new file mode 100644 index 0000000..c5a4826 Binary files /dev/null and b/maven/assets/mvnrepository.com.png differ