背景
最近使用 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}'")
}
出现问题
我们执行上述的测试用例,发现执行结果不尽如意:
切面并没有生效。查询之后发现有其他同学遇到过相同的问题,但是他的解决方法并没有让我非常满意,如果需要 override 每个需要拦截的方法,那我还用切面干嘛...
解决问题
经过反复实验发现,execution() 针对的方法签名是最终实际调用的方法的签名,当切点定义在继承子类上,但子类并没有实现/重写相应的方法时,切面是无法生效的,其中涉及了 JDK 代理和 CGLIB 代理等等内容,就不详细展开了。所以当将切点定义成如下所示时,切面是能够正常织入。
@Pointcut("execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save*(..))")
但是像上面这样定义切面肯定是不行的,因为大部分的业务 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!")
}
测试结果如下:
可以看到,切面只在 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
}
测试后结果如下:
结果显而易见,切面和方法时串行的,在切面的耗时会直接体现在整个调用整体的耗时上,所以我们是不是不应该在切面里做过多的操作呢?