概念解释
AOP
:Aspect Oriented Programming
Aspect
:方面Oriented
:面向…的Programming
编程
之前我对模块化编程的认识,主要是局限在布局结构、目录结构上。比如Vue
的template
模板。
对方法的模块化编程,我之前的认识局限在封装共用的工具方法上。
但没想到函数方法也可以用类似于模板的方式模块化构建。
利用的就是AOP面向切面编程。对应软件设计模式中的“代理模式”,创建对象的代理对象,代理对象的原始操作。
可以在连接点处插入切面逻辑。
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:Target,通知所应用的对象
AOP配置
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
本文实现的是,通过注解,记录日志到数据库。
注解的作用是标注哪些方法需要插入切面逻辑。
- 创建文件夹
annotation
存放注解接口,用于标注切入点 - 创建文件夹
aop
存放的切面类,用于设计切面 - 在
mapper
文件夹下创建OperateLogMapper
接口,用于操作数据库 - 在
pojo
文件夹下创建OperateLog
实体类,用于映射user_log
表
自定义Log注解
Log
注解的作用仅用于标识。
添加@Retention(RetentionPolicy.RUNTIME)
指定注解运行时生效。
添加@Target(ElementType.METHOD)
指定注解作用在方法上。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserLog {
}
注解的类型是@interface
,就是在interface
之前加了个@
。
切入点表达式
可以使用与或非来组合切入点表达式
execution(……)
:根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
@annotation(……)
:根据注解匹配
本文中是通过注解匹配,也就是添加该注解的方法在执行完毕后会自动添加到日志。
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(top.wushf.server.annotation.UserLog)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
String jwt = request.getHeader("token");
Claims claims;
Integer operateUser = null;
try {
claims = JwtUtils.parseJwt(jwt);
operateUser = (Integer) claims.get("id");
} catch (Exception e) {
}
LocalDateTime operateTime = LocalDateTime.now();
String className = joinPoint.getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
long end = System.currentTimeMillis();
long costTime = end - begin;
OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);
operateLogMapper.insertUserLog(operateLog);
return result;
}
}
切入点的方法、类名、参数、返回值都可以通过joinPoint
获取。
但操作用户携带在request
的header
中。获取request
对象,解析jwt
令牌。
当前切入点的request
对象通过@Autowired
注解自动注入。
parseJwt
可以会抛出异常,需要放到try{}catch(){}
中。
mapper持久层
插入日志需要调用持久层中的insert方法,将日志记录插入到数据库中。
@Mapper
public interface OperateLogMapper {
@Insert("insert into user_log (operate_user,operate_time,class_name,method_name,method_params,return_value,cost_time)" + "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insertUserLog(OperateLog log);
}
之后可能会扩展多个模块,多个业务有各自的日志。所以实现的是insertUserLog
。插入到用户操作日志的数据库中。
pojo实体类
好处很多,便于维护、传参等等,不多赘述。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id;
private Integer operateUser;
private LocalDateTime operateTime;
private String className;
private String methodName;
private String methodParams;
private String returnValue;
private Long costTime;
}
Jwt生成与解析
黑马PPT用的是0.9
的老版本,新版本很多方法已经弃用。目前是0.12.5
版本。
//私钥
private final static String SECRET = "";
//过期时间,单位:秒
public final static int ACCESS_EXPIRE = 60 * 60 * 24 * 7;
//加密算法
private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;
//密钥实例
public static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes());
public static String generateJwt(Map<String, Object> claims) {
Date exprireDate = Date.from(Instant.now().plusSeconds(ACCESS_EXPIRE));
return Jwts.builder().claims(claims).expiration(exprireDate).issuedAt(new Date()).signWith(KEY, ALGORITHM).compact();
}
public static Claims parseJwt(String jwt) {
return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(jwt).getPayload();
}
较之前的版本,现在的版本应该是更统一化了。
创建和解析的过程,都是先创建builder
,利用builder
创建或解析jwt令牌。