1、AOP的原理
1.1、AOP简介
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于增强应用程序的模块性、可维护性和复用性。它允许你在程序中以声明的方式定义横切关注点(Cross-cutting Concerns),这些关注点会横跨应用程序的多个模块。横切关注点通常包括日志记录、事务管理、安全性等,它们与应用程序的业务逻辑并不直接相关,但却散布在整个应用程序中。
1.2AOP核心
AOP 的核心概念是切面(Aspect),它是一个模块化的单元,用于实现横切关注点。一个切面封装了特定横切关注点的代码逻辑,并将其与应用程序的其他部分分离开来。
(1)、切点(Pointcut):定义了在应用程序中哪些地方应该应用切面的逻辑。切点是通过表达式或者通配符来定义的,它确定了在程序执行过程中应该插入切面的位置。
(2)、通知(Advice):定义了切面在切点处执行的行为。通知包括了在切点之前、之后、或者环绕执行点的代码逻辑。
1.3AOP使用场景
(1)、Java中,AOP 通常通过代理模式实现。当应用程序调用一个被切入的方法时,AOP 框架会根据切点信息决定是否应该在调用前后执行切面的逻辑,如果需要执行,AOP 框架会在调用前或者后执行切面逻辑,然后再继续执行原始方法。
(2)、Spring 框架提供了强大的 AOP 支持,它允许你通过声明式的方式来定义切面,并将其应用于应用程序中的多个部分。通过 Spring AOP,你可以轻松地实现诸如事务管理、日志记录、安全性等横切关注点,而无需修改原始代码。
1.4AOP是哈(简单打开思路理解)
AOP的面向切面编程的目标就是分离关注点,举个简单的例子一个人在家中,每天都需要起床叠被子,收拾家务,做饭等一系列的的日常活动,但是但是他本身的目标是去上班,这些其余的家中的事情统统交给其他的仆人去做,每个仆人负责一项家中工作,你只需要干你自己的正事,其他事情别人帮你去做,每个人各司其职,灵活的组合,达到一种可配置的、可插拔的程序结构。
2、AOP基本核心概念
(1)、切面(Aspect):横切关注点的模块化单元。它包含了在程序中某个特定点执行的一组动作(@Aspect)。
(2)、连接点(JoinPoint):应用程序执行过程中可以插入切面的点。一般是方法的调用、异常处理抛出及变量的修改。
(3)、通知(Advice):切面在连接点上执行的动作。常见的通知类型包括前置(@Before连接之前执行)、后置通知(@AfterReturning连接之后执行)、环绕通知(@Around连接点之前和之后执行,可以控制连接点的执行)、异常通知(@AfterThrowing连接点抛出异常执行)、最终通知(@After连接点执行完成后执行,无论连接点是否正常执行都会执行)
(4)、切点(Pointcut):一组连接点的集合,用于定义切面在何处执行。注:切点可以使用表达式或其他方式来匹配连接点(@Pointcut)。
(5)、目标对象(Target):应用程序中包含业务逻辑的对象,即切面所影响的对象就是目标对象。
(6)、代理(Proxy):AOP框架创建的对象,用于封装目标对象并将切面逻辑织入到目标对象的执行流程中。代理对象拦截对目标对象的访问,并在必要时调用与切面相关的逻辑。代理对象可以在目标对象方法执行之前、之后或周围添加额外的行为
(7)、织入(Weacing):将切面的逻辑应用到目标对象的过程。AOP框架通过织入将切面逻辑与目标对象关联起来。织入可以在编译时、加载时或运行时进行。编译时织入在编译源代码时就已经完成了,加载时织入在类加载到虚拟机时进行,而运行时织入则是在应用程序运行时动态地将切面逻辑织入到目标对象中。
3、AOP实现
以下是使用AOP去记录接口的执行状态:
AOP导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.1所需知识点:
Java元注解(常用):
(1)、@Retention:指定注解的保留策略,即注解在编译时、运行时还是在类加载时被保留。有三个取值:RetentionPolicy.SOURCE(
源代码级别保留,编译器丢弃)、RetentionPolicy.CLASS(类级别保留,但不会加载到 JVM 中)、RetentionPolicy.PUNTIME(运行时保留,可以通过反射获取)。
(2)、@Target:指定注解可以应用的目标类型。它的取值包括类(ElementType.TYPE)、接口(ElementType.TYPE)、枚举(ElementType.TYPE)、字段变量(ElementType.FIELD)、方法(ElementType.METHOD)、参数(ElementType.PARAMETER)及构造(ElementType.CONSTRUCTOR)等。
(3)、@Documented:指定注解是否应该被 javadoc 工具记录。若一个注解被 @Documented 注解,则它的信息会被包含在生成的文档中。
(4)、@Inherited:指示注解是否应该被子类继承。若一个注解被 @Inherited 注解,则它的子类会自动继承该注解。
3.2代码尝试
实现效果:监控接口,并将接口调用的信息存储显示出来
接口上面使用自定义的注解(以UserController删除接口为例)生成此接口的日志信息于数据库中:@LogRecord在此为切面
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@DeleteMapping("/delete/{id}")
// @LogRecord注解在此充当一个门卫的作用-----即切面
@LogRecord("删除用户信息") // "删除用户信息"就是value
public Result deleteUserInfo(@PathVariable Integer id){
userService.deleteUserInfo(id);
return Result.success();
}
}
以下是一个简单Annotation注解类定义注解@LogRecord(将此当作切面的注解,有使用此注解的接口则作为切面):
@Target(ElementType.METHOD) // 表注解用于什么地方(ElementType.METHOD表作用于方法上面)
@Retention(RetentionPolicy.RUNTIME)// 表示运行时保存该注解信(RetentionPolicy.RUNTIME在运行时)
@Documented 此注解会被javadoc工具提取成为文档
public @interface LogRecord{
String value() default ""; //注解中有一个value属性,默认为空
// 可再定义多个属性
}
AOP实现操作日志记录(监控自定义的注解):
建立一个AOP的处理器即LogHandler
/*
* 日志处理器(处理切面,即注解@LogRecord)
* */
@Component
@Aspect //表示是aop中的一个处理器
public class LogAspect {
@Resource
private LogService logService;
@Around("@annotation(logRecord)") //创建一个切面,Around是围绕着
// ProceedingJoinPoint可以获取连接点的信息,并通过调用 proceed() 方法来执行目标方法
public Object doAround(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
// 获取操作内容,在注解中通已经定义的value,即在接口上面写的内容
// 获取操作时间、操作人、操作人ip
String content=logRecord.value();
String time= DateUtil.now();
String username="";
Admin user = JwtTokenUtils.getCurrentUser();
if (ObjectUtil.isNotEmpty(user)){
username=user.getName();
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip=request.getRemoteAddr();
// 执行具体的joinPoint接口
Result result= (Result) joinPoint.proceed();
// 添加信息到日志表
Log log=new Log(null,content,time,username,ip);
logService.addLogInfo(log);
return result;
}
}
记录登录接口信息时有个小坑(JwtTokenUtils.getCurrentUser()是获取的登陆完成之后的用户,但是想要捕获登录之后的用户,需要在proceed()方法执行之后拿取):
@Component
@Aspect
public class LogAspect {
@Resource
private LogService logService;
@Around("@annotation(logRecord)")
public Object doAround(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
String content=logRecord.value();
String time= DateUtil.now();
String username="";
Admin user = JwtTokenUtils.getCurrentUser();
if (ObjectUtil.isNotEmpty(user)){
username=user.getName();
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip=request.getRemoteAddr();
Result result= (Result) joinPoint.proceed();
// 需要在登陆完成后从获取的数据中取出用户
Object data=result.getData();
if (data instanceof Admin){
Admin admin= (Admin) data;
username=admin.getName();
}
Log log=new Log(null,content,time,username,ip);
logService.addLogInfo(log);
return result;
}
}
LogController类:
/*
*实现查询、增加、删除三个基础功能
**/
@RestController
@RequestMapping("/log")
public class LogController {
@Resource
private LogService logService;
@PostMapping("/add")
public Result add(@RequestBody Log log){
logService.addLogInfo(log);
return Result.success();
}
@GetMapping("/search")
public Result searchLogInfo(Params params){
// 分页查询PageInfo
PageInfo<Log> logInfo=logService.searchLogInfo(params);
return Result.success(logInfo);
}
@DeleteMapping("/delete/{id}")
public Result deleteLogInfo(@PathVariable Integer id){
logService.deleteLogInfo(id);
return Result.success();
}
}
LogService接口类:
public interface LogService {
PageInfo<Log> searchLogInfo(Params params);
void addLogInfo(Log log);
void deleteLogInfo(Integer id);
}
LogServiceImpl实现类:
@Service
public class LogServiceImpl implements LogService {
@Resource
private LogMapper logMapper;
@Override
public PageInfo<Log> searchLogInfo(Params params) {
PageHelper.startPage(params.getCurrentPage(), params.getPageSize());
List<Log> loglist=logMapper.searchLogInfo(params);
return PageInfo.of(loglist);
}
@Override
public void addLogInfo(Log log) {
logMapper.insertSelective(log);
}
@Override
public void deleteLogInfo(Integer id) {
logMapper.deleteByPrimaryKey(id);
}
}
LogMapper接口类:
@Mapper
public interface LogMapper extends BaseMapper<Log> {
List<Log> searchLogInfo(@Param("params") Params params);
}
LogMapper.xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lqd.backmanagement.dao.LogMapper">
<select id="searchLogInfo" resultType="com.lqd.backmanagement.entity.Log">
select * from log
<where>
<if test="params != null and params.content!=null and params.content!=''">
and content like concat('%',#{params.content},'%')
</if>
<if test="params != null and params.username!=null and params.username!=''">
and username like concat('%',#{params.username},'%')
</if>
</where>
</select>
</mapper>
Log实体类:
/*
* AOP记录
* */
@Table(name = "log")
@Data
@AllArgsConstructor
public class Log {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "content")
private String content;
@Column(name = "time")
private String time;
@Column(name = "username")
private String username;
@Column(name = "ip")
private String ip;
}
Params实体类(存放一些分页查询的所需字段及其他查询字段)
@Data
public class Params {
private String content;
private String username;
private Integer currentPage;
private Integer pageSize;
}
Result封装的数据返回类
//接口规范
@Data
public class Result {
private static final String SUCCESS="200";
private static final String ERROR="404";
private String code;
private String message;
private Object data;
public static Result success(){
Result result = new Result();
result.setCode(SUCCESS);
return result;
}
public static Result success(Object data){
Result result = new Result();
result.setCode(SUCCESS);
result.setData(data);
return result;
}
public static Result error(String message){
Result result = new Result();
result.setCode(ERROR);
result.setMessage(message);
return result;
}
}