Aop
前面根据实际项目做了一个日志切面的Demo出来SpringAop。发现自己会使用了,可以理论知识理解得还不是很到位,所有继续完善一下自己的知识体系。
1.Aop简介
AOP是Aspect Oriented Programming,即面向切面编程。
那什么是AOP?
我们先回顾一下OOP:Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。
而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。
要理解AOP的概念,我们先用OOP举例,比如一个业务组件UserService,它有几个业务方法:
- createUser:添加新的用户;
- updateUser:修改用户信息;
- deleteUser:删除此用户。
对于每个业务方法来说,除了业务逻辑,还需要安全检查、日志记录、参数校验、拦截xss攻击和事务处理,它的伪代码像这样:
public class UserService {
public void createUser(User user) {
securityCheck();
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log("created user: " + user);
}
}
继续编写updateUser(),伪代码如下:
public class UserService {
public void updateUser(User user) {
securityCheck();
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log("updated userInfo: " + user);
}
}
对于安全检查、日志、事务等代码,它们会重复出现在每个业务方法中。使用OOP,我们很难将这些四处分散的代码模块化。
考察业务模型可以发现,UserService关心的是自身的核心业务逻辑,但是整个完整的系统还必须要求关注安全检查、日志、事务、性能等功能,这些功能实际上“横跨”多个业务方法,为了实现这些功能,不得不在每个业务方法上重复编写代码。
一种可行的方式是使用Proxy模式,将某个功能,例如,权限检查,放入到Proxy中:
public class SecurityCheckUserService implements UserService {
private final UserService target;
public SecurityCheckUserService (UserService target) {
this.target = target;
}
public void createUser(User user) {
securityCheck();
target.createUser(user);
}
public void updateUser(User user) {
securityCheck();
target.updateUser(user);
}
public void deleteUser(User user){
securityCheck();
target.deleteUser(user);
}
private void securityCheck() {
...
}
}
这种方式的缺点是比较麻烦,必须先抽取接口,然后,针对每个方法实现Proxy,要以后业务逻辑越来越庞大,写代理中的方法都得写吐血。
另一种方法是,既然SecurityCheckUserService的代码都是标准的代理模板代码,不如把权限检查视作一种切面(Aspect),把日志、事务、性能检测、验证参数、限流、拦截xss攻击等都视为切面,然后,以某种自动化的方式,把切面织入到核心逻辑中,实现Proxy模式。
如果我们以AOP的视角来编写上述业务,可以依次实现:
- 核心逻辑,即UserService;
- 切面逻辑,即:
权限检查的Aspect;
日志的Aspect;
事务的Aspect;
性能检测的Aspect...
然后,以某种方式,让框架来把上述对应的Aspect以Proxy的方式“织入”到UserService中,这样子就不必编写复杂而冗长的Proxy模式。

2.AOP原理
如何把切面织入到核心逻辑中?这正是AOP需要解决的问题。
在Java平台上,对于AOP的织入,有3种方式:
- 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
- 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
- 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。
最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB这些第三方库实现。
AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理,让我们把一些常用功能如权限检查、日志、事务等,从每个业务方法中剥离出来。
3 AOP术语及使用
切面(aspect)其实就是一个“何时(advice)” 在 ”何处(pointcut)” 干 “什么(advice)” 的一个理论。
3.1连接点(Join point)
连接点表示具体要拦截的方法。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。
切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
3.2切点(Poincut)
3.2.1 定义:切点定义了从何处切入。
切点的定义会匹配通知所要织入的一个或多个连接点。
通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
eg:以下代码是在删除课程的时候切入
/**
* 删除课程是确认
*/
@Pointcut("execution(* com.bimstudy.service.ICourseService.deleteCourse(..))")
public void deletedCourseUpdateLineStep() {
}
3.2.2表达式:
可以看看官方文档

-
基本语法格式为:
execution(修饰符模式 ? 返回类型模式 方法名模式(参数模式)异常模式?) 除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。 -
表达式的含义
此表达式的解释
符号 | 含义 |
---|---|
execution() | 表达式的主体 |
第一个”*“符号 | 表示返回值的类型任意 |
com.xyz.service | 要切入的服务的包名 |
包名后面的" .. " | 表示当前包及子包 |
第二个”*“ | 表示类名,*即所有类 |
.*(..) | 表示任何方法名,括号表示参数,两个点表示任何参数类型 |
- 关于参数模式的一个说明
符号 | 含义 |
---|---|
(*) | 表示任务类型的参数 |
(..) | 表示任务类型的参数 |
(*,String) | 表示两个参数,第一个参数是任何类型的,第二个参数必须是String |
() | 表示没有参数 |
3.3通知(Advice)
切面必须要完成的工作即称为通知。通知定义了切面是什么以及什么时候使用。
有以下5种类型通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能,如果这里面抛出异常,那么目标方法就不执行了;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么,无论目标方法是否抛异常,此段代码都会执行;
- 返回通知(AfterReturning):在目标方法正常完成之后调用通知;
@AfterReturning(value = "deletedCourseUpdateLineStep()", returning = "rvt")
public void deletedCourse(JoinPoint joinPoint, Object rvt) {
Long courseId = (Long) joinPoint.getArgs()[0];
log.info("用户{{}}删除了课程{{}}。,更新相应的路线的完成情况", UserUtil.getCurrentUser().getId(), courseId);
updateStudyLineByCourse(courseId);
}
- 异常通知(AfterThrowing):在目标方法抛出异常后调用通知;
/**
* 修改用户的课程进度切面
*/
@Pointcut("execution(* com.bimstudy.service.IUserVideoProgressService.updateProgress(..))")
public void updateUserProgressAspect() {
}
@AfterThrowing(value = "updateUserProgressAspect()", throwing = "ex")
public void doAfterThrowingException(JoinPoint joinPoint, Exception ex) {
UserVideoProgressVO courseVo = (UserVideoProgressVO) joinPoint.getArgs()[0];
log.info("捕获用户已看过视频异常,更新课程{{}}相关的路线进度", courseVo.getCourseId());
if (ex instanceof BusinessException
&& BusinessExceptionCode.VIDEO_USER_COMPLETE_LIMIT.getInnerCode().equals(((BusinessException) ex).getCode())
&& ComUtil.isNotEmpty(courseVo)) {
update(courseVo.getCourseId());
}
}
- 环绕通知(Around)
既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值;
当需要改变目标方法的返回值时,只能使用Around方法;
虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。
EG:一个用Around修改目标方法参数的切面
注解
/**
* 标注需要织入部门id的方法
* @author caob
*/
@Target( { ElementType.METHOD } )
@Retention( RetentionPolicy.RUNTIME )
@Documented
public @interface WeaveDeptId {
Constant.RolesRelation value() default Constant.RolesRelation.Dept;
String name() default "deptId";
}
切面
这个切面的作用是当登录的账户是部门管理员的时候,需要去找到部分管理员的depetId并设置到要使用的接口中
/**
* 织入部门id
*/
@Slf4j
@Aspect
@Component
public class WeaveDeptIdAop {
@Autowired
private IUserService userService;
@Around("@annotation(weaveDeptId)")
public Object before(ProceedingJoinPoint jp, WeaveDeptId weaveDeptId) throws Throwable {
String name = weaveDeptId.name();
//首先获取方法名称列表
MethodSignature msg = (MethodSignature)jp.getSignature();
String[] paramName = msg.getParameterNames();
List<String> paramNameList = Arrays.asList(paramName);
//获取传入的参数
Object[] args = jp.getArgs();
//如果有deptId这个参数
if (paramNameList.contains(name)) {
//返回参数位置
Integer pos = paramNameList.indexOf(name);
Long paramValue = (Long)args[pos];
if (paramValue== null){
Subject subject = SecurityUtils.getSubject();
Object principals = subject.getPrincipals();
if (!ComUtil.isEmpty(principals)){
String token = principals.toString();
Long userNo = Long.valueOf(JWTUtil.getUserNo(token));
Long loginFromAppId = Long.valueOf(JWTUtil.getLoginFromAppId(token));
Long tenantId = Long.valueOf(JWTUtil.getTenantId(token));
boolean onlyTargetRole = userService.isOnlyTargetRole(userNo, tenantId, loginFromAppId,weaveDeptId.value());
log.info("是否仅为部门管理员:{}",onlyTargetRole);
if (onlyTargetRole){
//织入部门id
args[pos]=userService.getById(userNo).getDeptId();
}
}
}
}
Object result = jp.proceed(args);
return result;
}
}
Controller
/**
* 获取积分明细/获取积分排名
*/
@WeaveDeptId(name="departmentId")
@ApiOperation(value = "获取积分明细/获取积分排名"+LICHUNLAN)
@ApiImplicitParams({
@ApiImplicitParam(name = "pageIndex", value = "第几页" , dataType = "int",paramType="query"),
@ApiImplicitParam(name = "pageSize", value = "每页多少条" , dataType = "int",paramType="query"),
@ApiImplicitParam(name = "departmentId", value = "部门名Id", dataType = "Long", paramType = "query"),
@ApiImplicitParam(name = "userName", value = "姓名", dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "userCode", value = "员工编码", dataType = "String", paramType = "query")
})
@Log(action="findByPage",modelName= "UserPointController",description="获取积分明细/获取积分排名")
@RequiresPermissions("userPoint:find")
@GetMapping("/findByPage")
public ResponseModel<IPage<UserPointListVO>> findByPage(
@RequestParam(name = "pageIndex", defaultValue = "1", required = false) Integer pageIndex,
@RequestParam(name = "pageSize", defaultValue = "20", required = false) Integer pageSize,
@RequestParam(name = "departmentId", required = false) Long departmentId,
@RequestParam(name = "userName", required = false) String userName,
@RequestParam(name = "userCode", required = false) String userCode){
UserPointListVO userPointListVO = new UserPointListVO();
Page<UserPointListVO> page = new Page<>(pageIndex, pageSize);
userPointListVO.setTenantId(UserUtil.getCurrentTenantId());
userPointListVO.setUserName(userName);
userPointListVO.setDeptId(departmentId);
userPointListVO.setUserCode(userCode);
return ResponseHelper.succeed(userPointService.findUserPoint(page,userPointListVO));
}
3.4切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容----它是什么,在何时和何处完成其功能。
-
正常的执行顺序
image.png
-
异常的执行顺序
image.png
参考:
https://www.liaoxuefeng.com/wiki/1252599548343744/1310052317134882