夏有清风

最是清風明月知人意,涼爽了整個如詩的夏夜

Open Source, Open Mind,
Open Sight, Open Future!
  menu
7 文章
12547 浏览
0 当前访客
ღゝ◡╹)ノ❤️

AOP在继承子类上的使用

背景

最近使用 Spring Security 做用户登录时的合法性校验,由于用户的密码加解密可以交由 BCryptPasswordEncoder 管理,但是对用户信息的 save 及 update 操作如修改密码时,需要对密码进行一次加密动作。系统的 ORM 使用的是优秀开源框架 mybatis-plus,最终UserService的接口及实现如下:

interface UserService : IService<User>
@Service("userService")
class UserServiceImpl : ServiceImpl<UserDao, User>(), UserService

解决方案

第一反应当然是使用 Spring AOP 对 UserService 接口下的所有 save 或 update 方法做前缀匹配,建立切面,在切面上对用户的密码进行统一的加解密。

切面定义:

@Pointcut("execution(* com.chaosmin.razor.service.UserService.save*(..))")
fun encodePoint() {}

@Around("encodePoint()")
@Throws(Throwable::class)
fun around(joinPoint: ProceedingJoinPoint): Any {
    val user = joinPoint.args.first() as User
    println("Current password is '${user.password}'")
    user.password = bCryptPasswordEncoder.encode(user.password)
    return joinPoint.proceed()
}

测试方法:

@Test
@Transactional
@Rollback(true)
open fun save() {
    val user = User()
    user.name = "user"
    user.password = "password"
    userService.save(user)
    println("after save, user's password is '${user.password}'")
}

出现问题

我们执行上述的测试用例,发现执行结果不尽如意:

1.png

切面并没有生效。查询之后发现有其他同学遇到过相同的问题,但是他的解决方法并没有让我非常满意,如果需要 override 每个需要拦截的方法,那我还用切面干嘛...

解决问题

经过反复实验发现,execution() 针对的方法签名是最终实际调用的方法的签名,当切点定义在继承子类上,但子类并没有实现/重写相应的方法时,切面是无法生效的,其中涉及了 JDK 代理和 CGLIB 代理等等内容,就不详细展开了。所以当将切点定义成如下所示时,切面是能够正常织入。

@Pointcut("execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save*(..))")

2.png

但是像上面这样定义切面肯定是不行的,因为大部分的业务 service 都会继承这个父类,最终导致切面织入到所有的子类方法上。

那么,在上述表达式的基础上,增加判断条件,使用 target() 方法对特定的子类进行过滤是不是可行呢?

我们修改切点定义如下:

@Pointcut("target(com.chaosmin.razor.service.UserService) and execution(* save*(..))")

同时修改测试用例:

@Test
@Transactional
@Rollback(true)
open fun save() {
    val user = User()
    userService.save(user.apply { name = "admin";password = "admin" })
    println("after save, user's password is '${user.password}'")
    aluminumPlateService.save(AluminumPlate().apply { name = "1" })
    println("Test OK!")
}

测试结果如下:

3.png

可以看到,切面只在 UserService 的 save 方法上生效了。

彩蛋

观察仔细的同学可能已经发现,织入切面之后,save() 方法的耗时从451ms增加到了5s多,那么,是不是由于织入了切面而导致的耗时增加呢?

答案当然是否定的,其中增加的耗时是因为 BCryptPasswordEncoder 进行了加密动作而导致的。现在我们换个思路,假如我在joinPoint.proceed()调用之后,增加一个延时,save() 方法的返回是否会被延后呢?

@Around("encodePoint()")
@Throws(Throwable::class)
fun around(joinPoint: ProceedingJoinPoint): Any {
    val user = joinPoint.args.first() as User
    println("Current password is '${user.password}'")
    // user.password = bCryptPasswordEncoder.encode(user.password)
    val a = joinPoint.proceed()
    println("${System.currentTimeMillis()} - ${JsonUtil.encode(a)}")
    Thread.sleep(1000)
    return a
}

测试后结果如下:

4.png

结果显而易见,切面和方法时串行的,在切面的耗时会直接体现在整个调用整体的耗时上,所以我们是不是不应该在切面里做过多的操作呢?