一、AOP简介
(一)核心概念
AOP(Aspect Oriented Programming)面向切面编程,一种编程思想,指导开发者如何组织程序结构。
作用:在不惊动原始设计的基础上为其进行功能增强。
(二)入门案例
- 导入坐标(spring的aop默认包含在context包中,所以不用导那个)
<!-- 复制aop,aspectj公司的aop做的比spring好,所以整合 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
-
创建连接点方法(就是正常写需要用的方法)
-
创建通知和通知类(见第四步)
-
定义切入点(在通知类中),将通知和切入点绑定(即切面)
//告诉Spring要加载这个类
@Component
//告诉Spring这个类用来做AOP
@Aspect
public class MyAdvice {
//定义切入点,私有且无参无返回值
@Pointcut("execution(void com.fn.dao.BookDao.update())")
private void pt() {
}
//将切入点和通知进行绑定
@Before(("pt()"))
public void method() {
System.out.println(System.currentTimeMillis());
}
}
- 在SpringConfig中告诉Spirng,要用注解开发AOP。
@EnableAspectJAutoProxy
二、AOP工作流程
三、AOP切入点表达式
(一)语法格式
(二)通配符
(三)书写技巧
四、AOP通知类型
@Before
:通知方法在原始切入点方法前运行。@After
:通知方法在原始切入点方法后运行。@Around
常用:通知方法在原始切入点方法前后运行。
eg:
@Around("pt()")
//如果原始方法有返回值,这里就写Object,没有就void
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("原始切入点前的动作");
//等价于原始切入点方法里的内容(如果原始方法有返回值,这里接收,并跟着返回)
Object ret = pjp.proceed();
System.out.println("原始切入点后的动作");
return ret;
}
使用@Around
注意事项
4. @AfterReturning
:通知方法在原始切入点方法正常执行完毕后运行(即原始方法没有异常或其他中断时执行)。
5. @AfterThrowing
:通知方法在原始切入点方法出现异常后运行(原始方法出异常了才执行)。
案例
数据库万次查询效率:
五、AOP通知获取数据
(一)形参
JoinPoint
代表连接点,可以获取连接点的信息,如方法名、参数等,但无法控制目标方法的执行。ProceedingJoinPoint
继承自JoinPoint
,同时还可以控制目标方法的执行,通过调用proceed()
方法来继续执行目标方法。
(二)返回值
只有@AfterReturning
和@Around
有返回值
(三)异常
(四)案例
- 各个类之间大量相同的功能,可以提出来做成AOP。
步骤:
- 创建
@interface
类型的注解。
import java.lang.annotation.*;
/**
* @Author: fn
* @Date: 2024/11/28 14:09
* @Description: 登录时,密码失败次数限制
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableLoginLimit {
/**
* 允许密码错误次数
*
* @return {int}
*/
int allowErrorCount() default 5;
/**
* 锁定账户时长(分钟)
*
* @return {int}
*/
int lockMinutes() default 10;
}
- 创建通知类 --> 在类中定义切入点同注解绑定 --> 定义切面方法写增强逻辑。
import cn.hutool.core.text.StrFormatter;
import com.hisi.cloud.authority.controller.RewriteTokenEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: fn
* @Date: 2024/11/28 14:21
* @Description: EnableLoginLimit AOP
**/
@Slf4j
@Aspect
@Component
public class EnableLoginLimitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 登录传参用户名字段
private static final String USER_NAME_PARAM = "username";
// 登录失败redis key
private static final String PWD_ERROR_PREFIX = "login:pwdFailureLimit:%s";
private static final String DISABLED_TEXT = "User is disabled";
/***
* 切入点
* execution(public * com.hisi.base.controller.*.*(..)) 解释:
* 第一个* 任意返回类型
* 第三个* 类下的所有方法
* ()中间的.. 任意参数
*
* annotation() 解释:
* 标记了@EnableLoginLimit 注解的方法
*/
@Pointcut("execution(public * com.hisi.cloud.authority.controller.RewriteTokenEndpoint.postAccessToken(..)) && @annotation(com.hisi.cloud.authority.configuration.login.EnableLoginLimit)")
private void loginLimitPointcut() {
}
/**
* 定义切面
* 假的登录限制,因为是在spring security比对密码抛出异常后,才进行的切面
*/
@Around("loginLimitPointcut()")
public Object loginLimitAspectMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解参数
EnableLoginLimit targetAnnotation = this.getTargetAnnotation(joinPoint);
if (targetAnnotation == null) {
return new RewriteTokenEndpoint.SwaggerEnhancer<>(400, null, "未获取到目标Annotation信息");
}
int allowErrorCount = targetAnnotation.allowErrorCount();
int lockMinutes = targetAnnotation.lockMinutes();
// 获取方法入参
Object[] param = joinPoint.getArgs();
Map<String, String> parameters = (Map<String, String>) param[1];
String userName = parameters.get(USER_NAME_PARAM);
try {
// 执行连接点方法,如果密码错误,spring security会抛出InvalidGrantException异常
Object proceed = joinPoint.proceed();
// 没有异常清空redis错误计数
stringRedisTemplate.delete(getKey(userName));
return proceed;
} catch (InvalidGrantException e) {
if (e.getMessage().contains(DISABLED_TEXT)) {
return new RewriteTokenEndpoint.SwaggerEnhancer<>(400, null, "用户已禁用");
}
Long errorCount = stringRedisTemplate.opsForValue().increment(getKey(userName));
stringRedisTemplate.expire(getKey(userName), lockMinutes, TimeUnit.MINUTES);
String resultMessage = "";
if (errorCount != null && errorCount > allowErrorCount) {
resultMessage = StrFormatter.format("密码错误超过{}次,账户已被锁定,请{}分钟后再试", allowErrorCount, lockMinutes);
} else {
resultMessage = StrFormatter.format("密码已错误{}次,超过{}次将锁定账户", errorCount, allowErrorCount);
}
return new RewriteTokenEndpoint.SwaggerEnhancer<>(400, null, resultMessage);
}
}
private String getKey(String key) {
return String.format(PWD_ERROR_PREFIX, key);
}
/**
* 获取连接点上注解
*
* @param joinPoint
* @return
*/
private EnableLoginLimit getTargetAnnotation(JoinPoint joinPoint) {
try {
EnableLoginLimit annotation = null;
if (joinPoint.getSignature() instanceof MethodSignature) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
if (method != null) {
annotation = method.getAnnotation(EnableLoginLimit.class);
}
}
return annotation;
} catch (Exception e) {
log.warn("获取 {}.{} 的 @EnableLoginLimit 注解失败", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), e);
return null;
}
}
}
六、事务
(一)简介
第一步:业务层使用@Transactional
注解
第二步:设置事务管理器(比如在JbdcConfig中写)
第三步:在SpringConfig开启注解式事务驱动@EnableTransactionManagement
(二)Spring事务原理
事务传播行为
简介:
事务的相关属性
对于propagation,有以下七种值: