add aop 模板

This commit is contained in:
Shuaishuai Dai 2023-02-16 16:15:01 +08:00
parent 3b41e8c168
commit ef8dd803cf
7 changed files with 393 additions and 74 deletions

View File

@ -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";
}
}
```

View File

@ -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";
}
}
```

View File

@ -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<String, String> visitMap = new LinkedTreeMap<>();
//判断用户权限是否存储再Redis中
if (StringUtils.isNotEmpty(visitUrlStr)) {
// 存在则讲redis取到的字符串进行序列化
visitMap = (LinkedTreeMap<String, String>) JsonUtils.jsonToMap(visitUrlStr, String.class);
} else {
// Redis中不存在则从数据库中获取并存储到Redis中
// 查询数据库获取获取用户权限列表
List<CategoryMenu> 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";
}
}
```

View File

@ -1,16 +1,15 @@
> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [javaguide.cn](https://javaguide.cn/tools/maven/maven-core-concepts.html)
> Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object ModelPOM) 的概念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 ModelPOM) 的概念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
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
```
你可以在 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
<project>
<dependencies>
<dependency>
@ -80,8 +77,6 @@
</dependency>
</dependencies>
</project>
```
**配置说明**
@ -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
<dependency>
<groupId>in.hocg.boot</groupId>
<artifactId>mybatis-plus-spring-boot-starter</artifactId>
<version>1.0.48</version>
</dependency>
<dependency>
<groupId>in.hocg.boot</groupId>
<artifactId>mybatis-plus-spring-boot-starter</artifactId>
<version>1.0.49</version>
</dependency>
```
若相同类型但版本不同的依赖存在于同一个 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
<dependencies>
...
dependency B
...
dependency D
</dependencies>
```
### [#](#排除依赖) 排除依赖
### 排除依赖
单纯依赖 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
<dependencyB>
......
<exclusions>
@ -223,8 +207,6 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 **
</exclusion>
</exclusions>
</dependency>
```
一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 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
<phases>
<phase>validate</phase>
@ -334,27 +314,18 @@ Maven 定义了 3 个生命周期`META-INF/plexus/components.xml`
<phase>deploy</phase>
</phases>
```
根据前面提到的阶段间依赖关系理论,当我们执行 `mvn test`命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。
### [#](#clean-生命周期) clean 生命周期
### clean 生命周期
clean 生命周期的目的是清理项目,共包含 3 个阶段:
1. pre-clean
2. clean
3. post-clean
```
```xml
<phases>
<phase>pre-clean</phase>
<phase>clean</phase>
<phase>post-clean</phase>
</phases>
<default-phases>
@ -362,24 +333,16 @@ clean 生命周期的目的是清理项目,共包含 3 个阶段:
org.apache.maven.plugins:maven-clean-plugin:2.5:clean
</clean>
</default-phases>
```
根据前面提到的阶段间依赖关系理论,当我们执行 `mvn clean` 的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。
### [#](#site-生命周期) site 生命周期
### site 生命周期
site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:
1. pre-site
2. site
3. post-site
4. site-deploy
```
```xml
<phases>
<phase>pre-site</phase>
<phase>site</phase>
@ -396,24 +359,22 @@ site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段
org.apache.maven.plugins:maven-site-plugin:3.3:deploy
</site-deploy>
</default-phases>
```
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
<build>
<plugins>
<plugin>
@ -435,10 +396,8 @@ jacoco-maven-plugin 使用示例:
</execution>
</executions>
</plugin>
</plugins>
</build>
</plugins>
</build>
```
你可以将 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)
[#](#文章推荐) 文章推荐
---------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB