最近跟朋友聊起接口优化…于是学习总结,记录下来。
本文目录
- 学习目标
- AOP
- AOP常用使用场景
- 开发准备
- 项目配置
- 访问接口限制
- 接口幂等性
- 其他场景
- 尾声
学习目标
- 看完这篇将会:
- 了解AOP
- 学会使用AOP控制接口访问以及接口幂等性
- 掌握AOP的使用场景
AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它允许将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来。通过AOP,可以将这些关注点模块化,并将它们应用到多个不同的组件和对象上,从而提高代码的可重用性和可维护性。在AOP中,关注点可以被定义为切面(aspect),切面是一组跨越不同类、不同层次的关注点。通常,一个切面由切(pointcut)和通知(advice)组成。
1. 切点:切点定义了在应用程序执行过程中哪些地方应该插入横切关注点。切点通常使用表达式来指定目标方法的选择。
2. 通知:通知定义了在切点处执行的逻辑。在AOP中,存在以下几种类型的通知:
3. 前置通知(Before Advice):在目标方法执行前执行的逻辑。
4. 后置通知(After Advice):在目标方法执行后执行的逻辑,无论方法是正常返回还是抛出异常。
5. 返回通知(After Returning Advice):在目标方法正常返回后执行的逻辑。
6. 异常通知(After Throwing Advice):在目标方法抛出异常后执行的逻辑。
7. 环绕通知(Around Advice):在目标方法执行前后都可以执行的逻辑。
AOP常用使用场景
8. 日志记录:通过在关键方法或操作的前后插入日志记录的切面,可以方便地记录系统的运行情况、用户的操作行为以及异常信息等。这对于调试、错误追踪和性能分析非常有帮助。
9. 安全控制:通过在敏感操作(如认证、权限校验)的前后插入安全控制的切面,可以实现统一的安全策略和权限控制,避免在每个方法中都进行权限验证。
10. 事务管理:通过在业务层方法的前后插入事务管理的切面,可以实现对数据库操作的自动事务管理。在方法执行前开启事务,在方法执行后根据执行结果提交或回滚事务,从而简化事务管理的代码,并保证数据的一致性。
11. 性能监控:通过在关键方法或操作的前后插入性能监控的切面,可以实时监控系统的性能指标,例如方法的执行时间、资源消耗等,从而找出性能瓶颈并进行优化。
12. 异常处理:通过在方法抛出异常时插入异常处理的切面,可以实现统一的异常处理逻辑。例如,可以记录异常日志、发送告警通知或返回给用户友好的错误信息。
13. 缓存管理:通过在方法执行前后插入缓存管理的切面,可以实现对方法返回结果的缓存。这样可以减少对底层资源的访问,提高系统的响应速度和性能。
14. 数据校验和转换:通过在参数校验、数据转换等操作的前后插入数据校验和数据转换的切面,可以实现统一的数据处理逻辑。例如,可以在保存数据之前进行数据的合法性验证或将不同数据格式进行转换。
开发准备
- Java 基础开发
- Maven 基本使用
- redis 基础使用
- 开发依赖版本
JDK 1.8.0 Maven 3.9.2
此项目中集成了redis,如果没有安装redis。可以自己去搜索安装教程。
项目配置
- pom.xml
因为项目中用到了AOP、Redis...所以引入对应的依赖文件
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- application.yml
########### 项目端口号设置 ##################
server:
port: 8080
########### Redis配置 ##################
spring:
redis:
host: 127.0.0.1
port: 6379
password:
如果redis设置了密码,在password后面加上密码即可
访问接口限制
- 首先自定义一个注解(RateLimit)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
}
- 创建一个AOP类
如果需要对整个controller下的API都做限制,可以使用execution表达式
@SuppressWarnings("all")
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
//定义 Redis Key 的前缀
private static final String KEY_PREFIX = "requestCount:";
@Autowired
private RedisTemplate redisTemplate;
//定义环绕通知
//将@annotation括号里的值换成自定义注释的所在路径
@Around("@annotation(com.example.annotate.RateLimit)")
public Object limitAccessCount(ProceedingJoinPoint joinPoint) throws Throwable {
//获取接口方法名称
String methodName = joinPoint.getSignature().toShortString();
/**
* 设置 redis 的 key
* RedisKey = Key前缀 + IP地址 + 接口名
*/
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//获取访问该方法用户对应的 ip 地址,在此作为唯一值
String ip = request.getRemoteAddr();
//拼接RedisKey
String redisKey = KEY_PREFIX + ip + methodName;
//每次访问都会 +1 ,直到 > 10,即一分钟内最多访问同一个接口10次
Long requestCount = redisTemplate.opsForValue().increment(redisKey, 1);
if (requestCount == 1) {
//设置过期时间为一分钟
redisTemplate.expire(redisKey, 1, TimeUnit.MINUTES);
}
//如果超过 10 次,会提示限制访问
if ((requestCount != null && requestCount > 10)){
log.error("ip为:" + ip + "的用户访问" + methodName + "接口次数超过限制");
//如果在限制期间再次访问该接口,会重置过期时间
redisTemplate.expire(redisKey, 1, TimeUnit.MINUTES);
return new Result(503, "系统繁忙,请稍后重试");
}
//如果条件不成立,将继续执行 controller 层的方法
return joinPoint.proceed();
}
}
- 在需要做接口限制的API上添加@RateLimit注释即可
- 效果展示
- 使用目的
通过限制接口的访问次数,可以避免系统因为过多的请求而过载。首先某些接口可能会暴露敏感数据或具有特殊功能,限制访问次数可以减少滥用或恶意攻击的风险。其次某些接口可能会占用较多的系统资源,如数据库查询、文件操作等。通过限制接口的访问次数,可以控制对这些资源的消耗,确保系统能够合理分配资源,并提供良好的服务质量。
接口幂等性
- 自定义注解(Idempotent)
/**
* 接口幂等性注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Idempotent {
}
- 创建一个AOP类
如果需要对整个controller下的API都做限制,可以使用execution表达式
@Slf4j
@SuppressWarnings("all")
@Component
@Aspect
public class RequestIdempotentAspect {
//保存每次请求的id
private Set<String> requestIdsAndTokens = new HashSet<String>();
Logger logger = LoggerFactory.getLogger(RequestIdempotentAspect.class);
/**
* Before 执行方法前, 检查 set 中是否存在 key
*
* @param joinPoint
*/
@Before("@annotation(com.example.springboot_aes.annotate.Idempotent)")
public void checkIdempotent(JoinPoint joinPoint) {
//获取 HttpServletRequest 对象, 利用 HttpServletRequest 获得请求头部参数
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//获取 header 的参数
String requestId = request.getHeader("X-Request-ID");
String token = request.getHeader("Cookie");
//拼接 set 的 key
String idempotentKey = createIdempotentKey(requestId, token);
//判断当前 key 是否存在与 set 中,如果存在,报一个 RuntimeException 异常
if (requestIdsAndTokens.contains(idempotentKey)) {
throw new RuntimeException("该请求正在处理中,请勿多次点击");
}
//如果当前 key 不存在于 set 中,将此 key 保存到 set 中
requestIdsAndTokens.add(idempotentKey);
}
/**
* After 执行完方法后,删除 set 中。此 key
*
* @param joinPoint
*/
@After("@annotation(com.example.springboot_aes.annotate.Idempotent)")
public void clearIdempotent(JoinPoint joinPoint) {
//获取 HttpServletRequest 对象, 利用 HttpServletRequest 获得请求头部参数
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//获取 header 的参数
String requestId = request.getHeader("X-Request-ID");
String token = request.getHeader("Cookie");
//拼接 set 的 key
String idempotentKey = createIdempotentKey(requestId, token);
//任务执行结束之后,删除 set 中对应的的key
requestIdsAndTokens.remove(idempotentKey);
}
/**
* 将请求ID和令牌组合成键
* @param requestId
* @param token
* @return String
*/
private String createIdempotentKey(String requestId, String token) {
return requestId + "-" + token;
}
- 在需要做接口限制的API上添加@Idempotent注释即可
- 效果展示
- 使用目的
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性
其他场景
这里举个小例子,如日志记录…
@Aspect
@Component
public class AopLog {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//使用环绕通知
@Around("execution(* com.example.controller.*.*(..))")
public Object myLogger(ProceedingJoinPoint pjp) throws Throwable {
//获取当前时间
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String className = pjp.getSignature().getDeclaringTypeName();
String methodName = pjp.getSignature().getName();
//使用数组来获取参数
Object[] array = pjp.getArgs();
ObjectMapper mapper = new ObjectMapper();
//执行函数前打印日志
logger.info("当前用户id为:" + StpUtil.getLoginIdDefaultNull());
logger.info("调用前:{}:{},传递的参数为:{}", className, methodName, mapper.writeValueAsString(array));
logger.info("URL:{}", request.getRequestURL().toString());
logger.info("IP地址:{}", request.getRemoteAddr());
//调用整个目标函数执行
Object obj = pjp.proceed();
//执行函数后打印日志
logger.info("调用后:{}:{},返回值为:{}", className, methodName, mapper.writeValueAsString(obj));
logger.info("耗时:{}ms", System.currentTimeMillis() - startTime);
// 此处写存库逻辑...
return obj;
}
}
尾声
相信看过这篇文章的应该可以理解到以下两点…
AOP的作用及其优势
- 作用:在程序运行期间,在不修改源码的基础上对方法进行功能增强。
- 优势:减少重复代码,提高开发效率,并且便于维护。
如果对接口优化
感兴趣的可以去网上学习,我这里给大家提供几个常用方案。。。
- 批处理
- 异步处理
- 空间换时间
- 预处理
- 池化思想
- 串行改并行
- 索引
- sql优化
好了,就先写到这里,如果有需要源码的可以在评论区评论。我会上传到git上供大家学习。