1,控制用户对系统资源(URI)的操作。前端的权限控制:对页面或页面元素的权限控制。后端的权限控制:对接口及数据的权限控制。
访问权限:哪些页面可以访问、哪些页面元素可见等等;操作权限:如页面按钮是否可点击、是否可以增删改查等等;接口与数据权限:接口是否可以调用、接口具体字段范围等等。
2,RBAC权限控制模型(Role-Based Access Control):基于角色的权限控制。RBAC模型的层级由低到高为:RBAC0、RBAC1、RBAC2、RBAC3。RBAC权限模型核心授权逻辑如下:某用户是什么角色;某角色具有什么权限;通过角色的权限推导用户的权限。
一个用户有一个或多个角色;一个角色包含多个用户;一个角色有多种权限;一个权限属于多个角色。用户:注册用户;角色:Lv0~Lv2会员;权限:视频投稿、发布动态、各种弹幕功能等等的权限;资源:页面、页面元素;操作:按钮点击、页面跳转、数据增删改查等等。
1,四张核心表,三张关联表。后续权限变更只需要更改连接表,关键信息表不变,方便拓展。
2,核心表:用户表存储用户(id
,phone
);角色表存储可选的用户等级(lv0,lv1,lv2);页面元素操作表存储可对前端页面上某个元素进行的操作(比如可点击的上传视频按钮);页面访问表存储页面(比如购买邀请码的页面)。为加速查询为经常匹配字段建立索引。
x1-- ----------------------------
2-- Table structure for t_auth_role
3-- ----------------------------
4DROP TABLE IF EXISTS `t_auth_role`;
5CREATE TABLE `t_auth_role` (
6 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
7 `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色名称',
8 `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色唯一编码',
9 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
10 `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
11 PRIMARY KEY (`id`) USING BTREE
12) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='权限控制--角色表';
13
14-- ----------------------------
15-- Table structure for t_auth_menu
16-- ----------------------------
17DROP TABLE IF EXISTS `t_auth_menu`;
18CREATE TABLE `t_auth_menu` (
19 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
20 `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单项目名称',
21 `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '唯一编码',
22 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
23 `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
24 PRIMARY KEY (`id`) USING BTREE
25) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='权限控制-页面访问表';
26
27-- ----------------------------
28-- Table structure for t_auth_element_operation
29-- ----------------------------
30DROP TABLE IF EXISTS `t_auth_element_operation`;
31CREATE TABLE `t_auth_element_operation` (
32 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
33 `elementName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '页面元素名称',
34 `elementCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '页面元素唯一编码',
35 `operationType` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作类型:0可点击 1可见',
36 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
37 `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
38 PRIMARY KEY (`id`) USING BTREE
39) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='权限控制--页面元素操作表';
3,连接表:用户-角色关联表t_user_role
存储用户与角色的关联关系(指明用户所处等级);角色-元素操作关联表t_auth_role_element_operation
存储角色与页面元素操作间关联(指明不同等级与不同可操作元素间关系,比如lv0不能点击视频上传按钮,lv1可以点击);角色-页面关联表t_auth_role_menu
存储角色与可访问页面的关系(指明角色与页面间的可访问关系,比如lv0不能访问邀请码购买页面,lv1可以正常访问)。由于关联表关联两个表的主键,需要添加外键约束。同时为加速查询为经常匹配字段建立索引。
xxxxxxxxxx
451-- ----------------------------
2-- Table structure for t_user_role
3-- ----------------------------
4DROP TABLE IF EXISTS `t_user_role`;
5CREATE TABLE `t_user_role` (
6 `id` bigint NOT NULL AUTO_INCREMENT,
7 `userId` bigint DEFAULT NULL COMMENT '用户id',
8 `roleId` bigint DEFAULT NULL COMMENT '角色id',
9 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
10 PRIMARY KEY (`id`) USING BTREE,
11 INDEX userId_index ( `userId`)
12) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户角色关联表';
13alter table `t_user_role` add constraint FK_user_role_userId foreign key (`userId`) references `t_user`(`id`);
14alter table `t_user_role` add constraint FK_user_role_roleId foreign key (`roleId`) references `t_auth_role`(`id`);
15
16-- ----------------------------
17-- Table structure for t_auth_role_element_operation
18-- ----------------------------
19DROP TABLE IF EXISTS `t_auth_role_element_operation`;
20CREATE TABLE `t_auth_role_element_operation` (
21 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
22 `roleId` bigint DEFAULT NULL COMMENT '角色id',
23 `elementOperationId` bigint DEFAULT NULL COMMENT '元素操作id',
24 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
25 PRIMARY KEY (`id`) USING BTREE,
26 INDEX roleId_index ( `roleId`)
27) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='权限控制--角色与元素操作关联表';
28alter table `t_auth_role_element_operation` add constraint FK_role_element_operation_roleId foreign key (`roleId`) references `t_auth_role`(`id`);
29alter table `t_auth_role_element_operation` add constraint FK_role_element_operation_elementOperationId foreign key (`elementOperationId`) references `t_auth_element_operation`(`id`);
30
31-- ----------------------------
32-- Table structure for t_auth_role_menu
33-- ----------------------------
34DROP TABLE IF EXISTS `t_auth_role_menu`;
35CREATE TABLE `t_auth_role_menu` (
36 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
37 `roleId` bigint DEFAULT NULL COMMENT '角色id',
38 `menuId` bigint DEFAULT NULL COMMENT '页面菜单id',
39 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
40 PRIMARY KEY (`id`) USING BTREE,
41 INDEX roleId_index ( `roleId`)
42) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='权限控制--角色页面菜单关联表';
43alter table `t_auth_role_menu` add constraint FK_role_menu_roleId foreign key (`roleId`) references `t_auth_role`(`id`);
44alter table `t_auth_role_menu` add constraint FK_role_menu_menuId foreign key (`menuId`) references `t_auth_menu`(`id`);
45
1,为方便之后根据关联表查询到与之关联表的内容,使用级联查询, 免得需要右表内容时再去数据库查询,加快响应速度,同时查询出对象体积变大,增大了存储需求,如果被包含对象体积较大,在大量查询时会带来较大压力。
级联查询:先确定所连接的表,再确定所要查询的字段,确定连接条件以及连接方式
inner join返回两个表基于连接条件实际匹配的行,即两个表交集。outer join返回两个表的并集结果,即匹配条件不满足的两个表的记录也将返回,mysql没有outer join 相关语句,但可以对left join和right join的结果用union连接来实现。
xxxxxxxxxx
21select * from A a inner join B b on a.ID = b.ID;
2select * from A a left join B b on a.ID = b.ID union select * from A a right join B b on a.ID = b.ID;
left join表示左侧表所有记录都将返回,并且不满足匹配条件的右侧连接表记录将返回null。
xxxxxxxxxx
11select * from A a left join B b on a.ID = b.ID;
right join与left join恰恰相反,表示右侧表所有记录都将返回,并且不满足匹配条件的左侧连接表记录将返回null。
xxxxxxxxxx
11select * from A a right join B b on a.ID = b.ID;
2,ibatis下级联查询实现
由于涉及到两张表,所以存在两个数据bean,常见做法是将右表bean放入左表bean,最后返回左表bean。如果右表字段较少,直接将右表字段直接加入左表bean。
xxxxxxxxxx
121<--右表字段较少,直接将右表字段直接加入左表bean。-->
2<select id="getUserRoleByUserId" parameterType="java.lang.Long" resultType="com.imooc.bilibili.domain.auth.UserRole">
3 select
4 ur.*,
5 ar.name roleName,
6 ar.code roleCode
7 from
8 t_user_role ur
9 left join t_auth_role ar on ur.roleId = ar.id
10 where
11 ur.userId = #{userId}
12</select>
如果右表结构复杂,需要设定resultMap将返回的多个平铺字段构建为右表bean,再嵌入左表bean返回。
xxxxxxxxxx
151<--定义复合结构组装方式-->
2<resultMap id="AuthElementOperationResultMap" type="com.imooc.bilibili.domain.auth.AuthRoleElementOperation">
3 <!--左表字段 -->
4 <id column="id" property="id"/>
5 <id column="roleId" property="roleId"/>
6 <id column="elementOperationId" property="elementOperationId"/>
7 <!-- 将左表bean嵌入右表bean-->
8 <association property="authElementOperation" javaType="com.imooc.bilibili.domain.auth.AuthElementOperation">
9 <!--右表字段 -->
10 <id column="elementName" property="elementName"/>
11 <id column="elementCode" property="elementCode"/>
12 <id column="operationType" property="operationType"/>
13 </association>
14</resultMap>
15
xxxxxxxxxx
171<--将返回的多个平铺字段构建为右表bean,再嵌入左表bean返回-->
2<select id="getRoleElementOperationsByRoleIds" parameterType="java.util.Set"
3 resultMap="AuthElementOperationResultMap">
4 select
5 areo.*,
6 aeo.elementName,
7 aeo.elementCode,
8 aeo.operationType
9 from
10 t_auth_role_element_operation areo
11 left join t_auth_element_operation aeo on areo.elementOperationId = aeo.id
12 where
13 areo.roleId in
14 <foreach collection="roleIdSet" item="roleId" index="index" open="(" close=")" separator=",">
15 #{roleId}
16 </foreach>
17</select>
UserAuthService
中通过UserRoleService
实现获得用户角色(等级)信息userRoleList
;通过AuthRoleService
查询用户对应的userRoleList
所拥有的页面元素操作权限RoleEleOps
与后台数据操作权限RoleMeau
。在获取到用户角色信息(等级信息)、页面元素操作权限信息、菜单操作权限信息后,前端就可以对按钮是否可点击做出限制,后端就可以根据预设权限规则对用户操作进行鉴权、拦截。
1,在进行后台操作前需要验证权限,无操作权限直接抛出异常并返回;验证成功才继续后续处理。上述流程对多个controller
通用,可以使用面向切面编程,将通用功能抽取,避免重复代码,使被影响代码更加专注于业务逻辑的处理。
2,实现功能:lv1及以上用户页面上发表动态按钮才是可点击的,并且lv2用户才可发表直播信息动态。对于按钮可点击限制,可能存在用户绕过前端无法点击按钮的限制直接访问接口的情形,定义注解ApiLimitedRole
用于拦截lv0用户直接访问发送动态接口的情形。定义注解DataLimitedAspect
用于拦截lv1用户发送直播动态的情形。
xxxxxxxxxx
81RetentionPolicy.RUNTIME) (
2ElementType.METHOD}) ({
3
4
5public @interface ApiLimitedRole {
6 // 被限制访问的等级,类似和名单
7 String[] limitedRoleCodeList() default {};
8}
xxxxxxxxxx
71RetentionPolicy.RUNTIME) (
2ElementType.METHOD}) ({
3
4
5public @interface DataLimited {
6
7}
3,定义切面ApiLimitedRoleAspect
,拦截有ApiLimitedRole
注解的方法,在进行业务的执行之前根据传入注解的限制等级集合,验证当前用户是否在黑名单中,如果在将报异常,否则正常进行后续流程。
xxxxxxxxxx
3411) (
2
3
4public class ApiLimitedRoleAspect {
5
6
7 private UserSupport userSupport;
8
9
10 private UserRoleService userRoleService;
11
12 // 拦截带有ApiLimitedRole的方法
13 "@annotation(com.imooc.bilibili.domain.annotation.ApiLimitedRole)") (
14 public void check() {
15 }
16
17 // 在带有ApiLimitedRole注解的方法执行前进行前置处理
18 "check() && @annotation(apiLimitedRole)") (
19 public void doBefore(JoinPoint joinPoint, ApiLimitedRole apiLimitedRole) {
20 Long userId = userSupport.getCurrentUserId();
21 // 用户等级ID
22 List<UserRole> userRoleList = userRoleService.getUserRoleByUserId(userId);
23 // 黑名单等级ID集合
24 String[] limitedRoleCodeList = apiLimitedRole.limitedRoleCodeList();
25 Set<String> limitedRoleCodeSet = Arrays.stream(limitedRoleCodeList).collect(Collectors.toSet());
26 Set<String> roleCodeSet = userRoleList.stream().map(UserRole::getRoleCode).collect(Collectors.toSet());
27 // 求交集,如果用户等级在黑名单中roleCodeSet,交集非空
28 roleCodeSet.retainAll(limitedRoleCodeSet);
29 if (roleCodeSet.size() > 0) {
30 throw new ConditionException("权限不足!");
31 }
32 }
33}
34
定义切面DataLimitedAspect
,拦截有DataLimited
注解的方法,在进行业务的执行之前判断拦截当前用户等级低于lv2且发送的动态类型为直播动态的情形,否则正常进行后续流程。
xxxxxxxxxx
3811) (
2
3
4public class DataLimitedAspect {
5
6
7 private UserSupport userSupport;
8
9
10 private UserRoleService userRoleService;
11
12 // 拦截带有DataLimitedAspect的方法
13 "@annotation(com.imooc.bilibili.domain.annotation.DataLimited)") (
14 public void check() {
15 }
16
17 // 在带有DataLimitedAspect注解的方法执行前进行前置处理
18 "check()") (
19 public void doBefore(JoinPoint joinPoint) {
20 Long userId = userSupport.getCurrentUserId();
21 // 用户等级ID
22 List<UserRole> userRoleList = userRoleService.getUserRoleByUserId(userId);
23 // 用户等级ID对应的编码
24 Set<String> roleCodeSet = userRoleList.stream().map(UserRole::getRoleCode).collect(Collectors.toSet());
25 Object[] args = joinPoint.getArgs();
26 // 遍历被注解方法参数,只对发布动态的方法起作用
27 for (Object arg : args) {
28 if (arg instanceof UserMoment) {
29 UserMoment userMoment = (UserMoment) arg;
30 String type = userMoment.getType();
31 // 发布直播动态且等级非lv2将报权限异常
32 if ("2".equals(type) && !roleCodeSet.contains(AuthRoleConstant.ROLE_LV2)) {
33 throw new ConditionException("lv2用户才能发布直播动态");
34 }
35 }
36 }
37 }
38}
在需要权限验证的地方添加注解
xxxxxxxxxx
41limitedRoleCodeList = {AuthRoleConstant.ROLE_LV0}) (
2
3"/user-moments") (
4public JsonResponse<String> addUserMoments( UserMoment userMoment) throws Exception
对于等级为0的用户发表动态时将提示权限不足:
对于等级为1的用户发表直播动态时时也将提示权限不足:
针对后续功能不断丰富,权限越来越多的情形,可以使用权限组的概念,同一个权限组中的不同用户拥有一系列相同的权限,简化多个权限挨个判断的情形。
1,常用功能分离形成可重用组件(日志),减少系统的重复代码,降低模块间的耦合度,集中管理维护方便,声明式(切面构成、将功能应用到要影响的组件中,无需修改受影响的类)编程较少模板代码,解耦,有利于未来的可拓展性和可维护性;核心代码更关注业务逻辑(高内聚、简单),无感知;
2,静态代理(在编译阶段就可生成 AOP 代理类,AspectJ),动态代理(spring AOP:运行时借助于JDK动态代理、CGLIB等在内存中“临时”生成AOP动态代理类,final修饰的类不能被代理,同样static和final修饰的方法也不会代理,因为static和final方法是不能被覆盖的)
3,通知(什么+时机):定义切面功能与出发时机;(bofore,after,around等)。连接点:程序中能够插入切面的点(方法调用、抛出异常),通过切入点添加新功能。切点(何处):表达式匹配要织入的连接点。切面(是什么+何时+何处)=通知+切点。织入:把切面应用到目标对象并创建代理对象的过程,运行期:创建动态代理、springAOP。
1,jdk动态代理:拦截接口方法,创建接口的代理实例,只支持方法连接点只能拦截方法;代理类包裹目标类,拦截方法调用,执行切面逻辑,转发给真正的bean。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
定义一个InvocationHandler实例,它负责实现接口的方法调用->通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:使用的ClassLoader,通常就是接口类的ClassLoader;需要实现的接口数组,至少需要传入一个接口进去;用来处理接口方法调用的InvocationHandler 实例->将返回的Object强制转型为接口。
xxxxxxxxxx
481public class DynamicProxy {
2 public static void main(String[] args) {
3 // 普通学生类
4 Student ordinaryStudents = new OrdinaryStudents();
5 ordinaryStudents.write();
6
7 /**
8 * InvocationHandler作用就是,当代理对象的原本方法被调用的时候,会重定向到一个方法,
9 * 这个方法就是InvocationHandler里面定义的内容,同时会替代原本方法的结果返回。
10 * InvocationHandler接收三个参数:proxy,代理后的实例对象。 method,对象被调用方法。args,调用时的参数。
11 */
12 InvocationHandler handler = (proxy, method, handlerArgs) -> {
13 // 从定义write方法。
14 if ("write".equals(method.getName())) {
15 System.out.println("增强型学生write()执行前");
16 method.invoke(ordinaryStudents, handlerArgs);
17 System.out.println("增强型学生write()执行后");
18 return null;
19 }
20 return null;
21 };
22 /**
23 * 对这个实例对象代理生成一个代理对象。
24 * 被代理后生成的对象,是通过People接口的字节码增强方式创建的类而构造出来的。它是一个临时构造的实现类的对象。
25 * loader和interfaces基本就是决定了这个类到底是个怎么样的类。InvocationHandler决定了这个代理类到底是多了什么功能.
26 * 通过这些接口和类加载器,拿到这个代理类class。然后通过反射的技术复制拿到代理类的构造函数,
27 * 最后通过这个构造函数new个一对象出来,同时用InvocationHandler绑定这个对象。
28 * 最终实现可以在运行的时候才切入改变类的方法,而不需要预先定义它。
29 */
30 Student sonOfDistrict = (Student) Proxy.newProxyInstance(ordinaryStudents.getClass().getClassLoader(), ordinaryStudents.getClass().getInterfaces(), handler);
31 sonOfDistrict.write();
32 }
33}
34/**
35 * 学生接口
36 */
37interface Student {
38 void write();
39}
40/**
41 * 普通学生
42 */
43class OrdinaryStudents implements Student {
44
45 public void write() {
46 System.out.println("我在写作文!");
47 }
48}
2,cglib代理:目标对象不是接口,字节码生成代理类继承目标类,并覆盖其中特定方法并添加增强代码,所以final修饰的类不能被代理,同样static和final修饰的方法也不会代理。CGLib创建的代理对象性能比JDK动态代理创建的代理对象高很多,但花费的时间多,spectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理,cglib适合单例的对象代理创建,jdk动态代理合多例的对象代理创建。
SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。如果需要默认使用 JDK 动态代理可以通过配置项 spring.aop.proxy-target-class=false来进行修改。
xxxxxxxxxx
391public class Dao {
2
3 public void update() {
4 System.out.println("PeopleDao.update()");
5 }
6}
7
8public class DaoProxy implements MethodInterceptor {
9
10 public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
11 //Object表示要进行增强的对象
12 //Method表示拦截的方法
13 //Object[]数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
14 //MethodProxy表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
15 System.out.println("Before Method Invoke");
16 proxy.invokeSuper(object, objects);
17 System.out.println("After Method Invoke");
18
19 return object;
20 }
21}
22
23public class CglibTest {
24
25 public void testCglib() {
26
27 DaoProxy daoProxy = new DaoProxy();
28 Enhancer enhancer = new Enhancer();
29 // setSuperclass表示设置要代理的类
30 enhancer.setSuperclass(Dao.class);
31 // setCallback表示设置回调即MethodInterceptor的实现类
32 enhancer.setCallback(daoProxy);
33 // 使用create()方法生成一个代理对象
34 Dao dao = (Dao)enhancer.create();
35 dao.update();
36 dao.select();
37 }
38
39}
3,切点:切点的定义会匹配通知所要织入的一个或多个连接点,描述要连接的连接点进行匹配,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。spring AOP只能方法拦截(execution)。多条件与或非复杂逻辑,被影响的类无感知。
4,通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入SpringAOP的切面。
5,Spring AOP 属于运行时增强,动态代理,而 AspectJ 是编译时增强,静态代理。 Spring AOP 基于代理 (Proxying),而 AspectJ 基于字节码操作 (Bytecode Manipulation)。AspectJ 最完整的 AOP 框架了,可以实现方法、等段等拦截,spring AOP功能受限,但是 Spring AOP 相对来说更简单,如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。