AOP、注解、EL表达、若依权限,Security原理综合分析
案例一:更新、创建增强
需求产生
每个表中均有创建时间、创建人、修改时间、修改人等字段。
在操作时候手动赋值,就会导致编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答案是可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
实现思路
有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:
数据库字段名 | 数据库字段类型 | 实体类字段名 | 实体类字段类型 | 操作 |
---|---|---|---|---|
create_time | datetime | createTime | java.util.Date | 创建的时候赋值 |
create_by | varchar | createBy | String | 创建的时候赋值 |
update_time | datetime | updateTime | java.util.Date | 更新的时候赋值 |
update_by | varchar | updateBy | String | 更新的时候赋值 |
分析
- 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
- 使用。在 Mapper 层需要为createTime、createBy、updateTime、updateBy赋值的方法上加入 AutoFill 注解
操作
-
引入依赖
<!-- aspects切面 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency>
-
在common的constant包下创建AutoFillConstant类。一般常量我们都使用常量类来管理。
package com.ruoyi.common.constant; /** * @Author yimeng * @Date 2024/4/29 9:45 * @PackageName:com.ruoyi.common.constant.autoFill * @ClassName: AutoFillConstant * @Description: 公共字段自动填充相关常量 * @Version 1.0 */ public class AutoFillConstant { /** * 实体类中的方法名称 */ public static final String SET_CREATE_TIME = "setCreateTime"; public static final String SET_UPDATE_TIME = "setUpdateTime"; public static final String SET_CREATE_BY = "setCreateBy"; public static final String SET_UPDATE_BY = "setUpdateBy"; }
-
在common的enums包下创建OperationType类。一般用于可选一类东西的时候,都用枚举,就像下拉框一样,你可以在多个同类值中选择你要的,你就用下拉框,后端的话,我们一般可以用枚举类。
package com.ruoyi.common.enums; /** * @Author yimeng * @Date 2024/4/29 9:46 * @PackageName:com.ruoyi.common.enums * @ClassName: OperationType * @Description: 数据库操作类型 * @Version 1.0 */ public enum OperationType { /** * 插入操作 */ INSERT, /** * 更新操作 */ UPDATE, }
-
在annotation包下创建AutoFill注解。
package com.ruoyi.common.annotation; import com.ruoyi.common.enums.OperationType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @Author yimeng * @Date 2024/4/29 9:49 * @PackageName:com.ruoyi.common.annotation * @ClassName: AutoFill * @Description: 自定义注解,用于标识某个方法需要进行功能字段自动填充处理 * @Version 1.0 */ @Target(ElementType.METHOD)//指定这个注解只能加在方法上面 @Retention(RetentionPolicy.RUNTIME)//JVM 在运行时保留该注解,并允许通过反射访问。 public @interface AutoFill { //通过枚举指定数据库操作方式:update insert OperationType value(); }
-
在aspect包下创建AutoFillAspect自定义切面类
package com.ruoyi.common.aspect; import com.ruoyi.common.annotation.AutoFill; import com.ruoyi.common.constant.AutoFillConstant; import com.ruoyi.common.enums.OperationType; import com.ruoyi.common.utils.SecurityUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Date; /* 问题1:数据库字段的命名 答:create_time、update_time、create_by、update_by 问题2:数据库字段的类型(和上面对应) 答:datetime、datetime、varchar、varchar 问题3:实体类字段的类型(和上面对应) 答:Date、Date、String、String 问题3:创建的时候给更新时间吗? 答:创建的时候给更新时间和创建时间 问题4:使用 答:在 com.ruoyi.任意深度的包.mapper 层的方法上加上注解@AutoFill。约定方法的实参第一个要是被操作的表对应的实体类。注解可以选择的value值为INSERT、UPDATE。 问题5:各注解的效果 答: @AutoFill(INSERT) 创建的时候自动填充create_time、create_by、update_time、update_by字段 @AutoFill(UPDATE) 更新的时候自动填充update_time、update_by字段 */ /** * @Author yimeng * @Date 2024/4/29 9:53 * @PackageName:com.ruoyi.common.aspect * @ClassName: AutoFillAspect * @Description: 自定义切面,实现公共字段自动填充处理逻辑 * @Version 1.0 */ @Aspect//声明是切面 @Component//bean交给spring容器管理 //@Slf4j public class AutoFillAspect { /** * 定义切入点 */ //切点表达式(com.ruoyi..mapper: 表示匹配com.ruoyi包下任意深度子包中的mapper包。*.*: 表示匹配mapper包下所有类的所有方法。@annotation(com.ruoyi.common.annotation.AutoFill): 表示匹配带有com.ruoyi.common.annotation.AutoFill注解的方法。要两个条件都满足才能被这个方法切入)。这里写的这个方法名是任意的,叫什么都行,不用写什么方法体内容,这里主要是把execution(* com.ruoyi..mapper.*.*(..)) && @annotation(com.ruoyi.common.annotation.AutoFill)进行抽取而已,然后下面的@Before("autoFillPointCut()")就相当于是写了@Before("execution(* com.ruoyi..mapper.*.*(..)) && @annotation(com.ruoyi.common.annotation.AutoFill)"). @Pointcut("execution(* com.ruoyi..mapper.*.*(..)) && @annotation(com.ruoyi.common.annotation.AutoFill)") public void autoFillPointCut(){ } /** * 定义前置通知,在通知中进行公共字段的赋值 */ //前置通知,在执行操作之前.注意:前置通知执行后会自动执行我们的目标方法的,所以这里不用我们在这个增强方法里面调用目标方法去执行。不像环绕通知,环绕通知需要我们手动去调用目标方法,不然将不会自动去执行目标方法 @Before("autoFillPointCut()")//匹配上切点表达式的时候,执行通知方法。这里的@Before内写的是上面抽取的方法名。 public void autoFill(JoinPoint joinPoint){ //连接点,哪个方法被拦截到了,以及拦截到的方法参数值和类型 // log.info("开始进行公共字段自动填充..."); /** * 通知内容 */ //1.获取到当前被拦截的方法上的数据库操作类型 MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象,Signature是接口转为MethodSignature子接口 AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象 OperationType operationType = autoFill.value();//获得写在mapper方法上的数据库操作类型。被OperationType注解管理。 //2.获取到当前被拦截的方法的参数,也就是实体对象 Object[] args = joinPoint.getArgs();//获得连接点的参数,有多个参数,约定第一个参数是实体 if(args == null || args.length == 0){ return; } Object entity = args[0];//数据库操作方法中,约定实体类必须是第一个参数,获取实体 //3.准备赋值的数据 Date now = new Date(); String userName = SecurityUtils.getUsername(); //4.获取class对象 Class<?> clazz = entity.getClass(); //5.根据当前不同的操作类型,为对应的属性通过反射来赋值 if(operationType == OperationType.INSERT){ //为公共字段赋值 try { //获取set方法 //规范化,防止写错,所以将方法名写为常量类。 //注意:getDeclaredMethod方法不能获取到父类继承的方法,但是getMethod可以获取到,所以这里用getMethod方法。 // Method setCreateTime = clazz.getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, Date.class); Method setCreateTime = clazz.getMethod(AutoFillConstant.SET_CREATE_TIME, Date.class); Method setCreateBy = clazz.getMethod(AutoFillConstant.SET_CREATE_BY, String.class); Method setUpdateTime = clazz.getMethod(AutoFillConstant.SET_UPDATE_TIME, Date.class); Method setUpdateBy = clazz.getMethod(AutoFillConstant.SET_UPDATE_BY, String.class); //通过反射为对象属性赋值 setCreateTime.invoke(entity,now); setCreateBy.invoke(entity,userName); setUpdateTime.invoke(entity,now); setUpdateBy.invoke(entity,userName); } catch (Exception e) { e.printStackTrace(); } }else if(operationType == OperationType.UPDATE){ //为公共字段赋值 try { //获取set方法 Method setUpdateTime = clazz.getMethod(AutoFillConstant.SET_UPDATE_TIME, Date.class); Method setUpdateBy = clazz.getMethod(AutoFillConstant.SET_UPDATE_BY, String.class); //通过反射为对象属性赋值 setUpdateTime.invoke(entity,now); setUpdateBy.invoke(entity,userName); } catch (Exception e) { e.printStackTrace(); } } } }
使用:
public interface SysMenuMapper{ /** * 新增菜单信息 * * @param menu 菜单信息 * @return 结果 */ @AutoFill(value = OperationType.INSERT) public int insertMenu(SysMenu menu); }
原来代码:
原来效果:
原因是因为控制层给了操作人姓名:
注释后的效果:
使用我们的注解后的效果(这个时候service层的setCreateBy()还是被注释的状态哈):
注意:只有加在mapper层,注解才会生效。
展示一下在其他地方使用这个注解:
注意:上面为什么我们没有给时间,结果也有创建时间呢?
因为这个版本的若依的时间是在sql中写的:
案例二:数据权限增强
前置知识点(AOP的其他写法)
案例二我们要使用AOP实现权限管理。
Spring-aop实现切入有两种方式,一种是路径切入(使用execution(……)),一种是注解切入(使用@annotation(……))。(案例一中的既用了路径切入也用了注解切入,要满足两个条件才能进行切入)。
写法一:
写法二:
案例一中获取注解对象的做法是下面这样的:
分析:先抽取表达式,即那个切点表达式+注解,表达式写在随便一个方法上,方法的方法体没有意义,所以不写,写了也没有用。然后增强的内容上使用那个抽取的表达式。这样的效果就是在目标方法执行前,先进行增强,这里是前置增强(看@Before注解里面写的方法,代表上面的被抽取的表达式,这个表达式就是用于确定增强方法的。还有一点需要注意:AOP增强内容的这些方法,参数里面都可以直接写JoinPoint的,如果你要用到目标方法的一些东西,那么就直接写这个参数,然后去使用它的方法调用原方法就行了)。上面前置增强的效果是,在执行目标方法之前,会先执行autoFill(JoinPoint joinPoint)方法,autoFill(JoinPoint joinPoint)这个方法里面我们通过joinPoint拿到了方法上面的注解,然后利于注解中的值来进行对应的增强。注意,因为前置增强不需要调用目标方法也会自动去执行原来的目标方法。所以这里不用使用joinPoint调用原方法。
上面这种既展示了普通的切点表达式来进行匹配,也展示了普通的注解的方式来进行匹配方法,用了&&符号来让两个条件都满足才进行增强。因为@Pointcut也是支持SpringEL表达式的写法的,所以值写EL表达式也行,因为内部的解析程序考虑到了EL表达式的写法。
下面要展示的是另一种注解匹配的写法,也一样可以获取到注解中的内容。
相当于传了一个注解过来,然后注解中带着一些数据。
注意:这种写法的话,要求参数名和@annotation注解中的名字要一样才行(如果需要JoinPoint,那么就要把JoinPoint写在方法的第一个参数才行)。比如下面这样:
这种写法,就是,不用抽取。直接把注解写在方法参数里面,然后@annotation内写方法参数中的这个注解参数的参数名。这种写法spring能自动知道,要去切这个注解对应的方法。
上面的写法就相当于是下面这样的写法(下面这个写法和上面写法的执行结果是一样的,但是如果要获取注解中的数据,把注解写在方法形参里面的写法使用起来更加顺滑一点。):
案例引入
若依中是怎么来实现数据权限的?
我们先来看数据权限使用的地方:
controller层:
service接口,只有方法声明(没什么特殊的):
然后就是service层的实现了,实现的方法上面有一个注解。
(总之就是,注解的作用就相当于是把一些值放到了注解里面,然后注解挂在方法上面了而已,然后解析程序去拿这个数据而已,如果没有解析注解的程序,那么这个注解将不会有任何作用,只是挂一个信息到方法上而已。注解和注释的区别就是:注解是给程序看的,程序可以看到并使用这个信息,当然不使用也行。但是注释是给开发者看的,程序是看不到注释的信息的。注解的作用,其实就是挂一个信息到某个方法或者某个类上而已,到时候解析程序可以通过反射去拿到注解,并且获取到注解里面的数据,然后进行对应的解析操作。):
然后就是mapper接口(没有什么特殊的):
然后发现xml中有一个${params.dataScope}:
通过我们对xml中写占位符的经验,我们知道KaTeX parse error: Expected 'EOF', got '#' at position 53: …sql注入问题。那么为什么不用#̲{}呢?因为这个虽然不会sql…{},但是注意使用这个的话,我们代码中要注意排除sql注入问题。
因为,mapper层的方法长这样:
所以,我们可以确定${params.dataScope}表示的是,取dept对象中的params属性的dataScope属性值。
我们找了SysDept找不到params变量
但是,我们在父类中可以看到params变量了:
params.dataScope其实不止是取某个对象的某个属性,他也可以表示取“属性名叫params的map中键为dataScope的值”。
所以是OK的。
然后我们看看一个奇怪的现象:
我们看看请求:
我们看到执行的sql竟然被增强了,即params.dataScope被给了值
但是这个值是怎么来的呢?
答:AOP切入的。
课外知识点:java中 ${} 和 #{} 有什么区别
前言
${}
和 #{} 都是 MyBatis 中用来替换参数的,它们都可以将用户传递过来的参数,替换到 MyBatis 最终生成的 SQL 中,但它们区别却是很大的,接下来我们一起来看。1.功能不同
${} 是将参数直接替换到 SQL 中,比如以下代码:
<select id="getUserById" resultType="com.example.demo.model.UserInfo"> select * from userinfo where id=${id} </select>
最终生成的执行 SQL 如下:
从上图可以看出,之前的参数 ${id} 被直接替换成具体的参数值 1 了。
而 #{} 则是使用占位符的方式,用预处理的方式来执行业务。比如,我们将上面的案例改造为 #{} 的形式,实现代码如下:
<select id="getUserById" resultType="com.example.demo.model.UserInfo"> select * from userinfo where id=#{id} </select>
最终生成的 SQL 如下:
即,有问号的。
区别
比如下面代码:
<select id="getUserByName" resultType="com.example.demo.model.UserInfo"> select * from userinfo where name=${name} </select>
以上程序执行时,生成的 SQL 语句如下:
这样就会导致程序报错,因为传递的参数是字符类型的,而在 SQL 的语法中,如果是字符类型需要给值添加单引号,否则就会报错,而
${}
是直接替换,不会自动添加单引号,所以执行就报错了。前面的例子里面,没有加单引号也能正确,是因为,填充的是数值,数值在sql中不需要加什么引号,直接把值写上去就行了。上面出错的代码使用 #{} 来做就可以成功执行,因为 #{} 采用的是占位符预执行的,所以不存在任何问题,它的实现代码如下:
比如:
<select id="getUserByName" resultType="com.example.demo.model.UserInfo"> select * from userinfo where name=#{name} </select>
以上程序最终生成的执行 SQL 如下:
但是在我们需要传一个sql关键字给xml中的sql的时候,占位符就做不到了。
比如,当我们要根据价格从高到低(倒序)、或从低到高(正序)查询,并且排序可以切换时,如下图所示:
这时我们就需要传递排序的关键字了。即,要传sql中的一些sql字符,而不是只传值的时候,我们可以使用
${}
来实现:比如:
<select id="getAll" resultType="com.example.demo.model.Goods"> select * from goods order by price ${sort} </select>
以上代码生成的执行 SQL 和运行结果如下:
但是,如果将代码中的 ${} 改为 #{},那么程序执行就会报错,#{} 的实现代码如下:
<select id="getAll" resultType="com.example.demo.model.Goods"> select * from goods order by price #{sort} </select>
以上代码生成的执行 SQL 和运行结果如下:
总结
**从上述的执行结果我们可以看出:**当传递的是普通参数时,需要使用 #{} 的方式,而当传递的是 SQL 命令或 SQL 关键字时,需要使用
${}
来对 SQL 中的参数进行直接替换并执行。除此之外,
${}
和 #{} 最主要的区别体现在安全方面,当使用${}
会出现安全问题,也就是 SQL 注入的问题,而使用 #{} 因为是预处理的,所以不会存在安全问题。这个例子就不举了,之前学习的时候学过。
若依AOP如何做到数据权限
内容1
下面我们来探究一下若依AOP如何做到数据权限的。
点击查询
控制层方法看到param中还没有数据:
在service层中就看到了数据:
说明在这个service层原来的方法体中的语句执行前就执行了增强语句。
所以我们找能切到这个方法的切面类:
package com.ruoyi.framework.aspectj;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.