AOP、注解、EL表达、若依权限,Security原理综合分析

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 更新的时候赋值

分析

  1. 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
  2. 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
  3. 使用。在 Mapper 层需要为createTime、createBy、updateTime、updateBy赋值的方法上加入 AutoFill 注解

操作

  1. 引入依赖

    <!-- aspects切面 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
    </dependency>
    

    image-20240503224835486

  2. 在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";
    }
    

    image-20240503224853098

  3. 在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,
    }
    

    image-20240503224916667

  4. 在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();
    }
    

    image-20240503224936223

  5. 在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();
                }
            }
        }
    }
    

    image-20240503224553485

    使用:

    public interface SysMenuMapper{
         
        /**
         * 新增菜单信息
         *
         * @param menu 菜单信息
         * @return 结果
         */
        @AutoFill(value = OperationType.INSERT)
        public int insertMenu(SysMenu menu);
    }
    

    image-20240503224612998

    原来代码:

    image-20240503223011098

    image-20240503224703038

    原来效果:

    image-20240503222548734

    image-20240503222937086

    原因是因为控制层给了操作人姓名:

    image-20240503224738302

    注释后的效果:

    image-20240503223034740

    image-20240503223103677

    image-20240503223437627

    使用我们的注解后的效果(这个时候service层的setCreateBy()还是被注释的状态哈):

    image-20240503223730871

    image-20240503223832034

    image-20240503223918962

    注意:只有加在mapper层,注解才会生效。

    展示一下在其他地方使用这个注解:

    image-20240503224016114

    image-20240503224120560

    image-20240503224228098

    image-20240503224317905

    注意:上面为什么我们没有给时间,结果也有创建时间呢?

    因为这个版本的若依的时间是在sql中写的:

    image-20240503224437708

案例二:数据权限增强

前置知识点(AOP的其他写法)

案例二我们要使用AOP实现权限管理。

Spring-aop实现切入有两种方式,一种是路径切入(使用execution(……)),一种是注解切入(使用@annotation(……))。(案例一中的既用了路径切入也用了注解切入,要满足两个条件才能进行切入)。

写法一:

image-20240506224553919

写法二:

image-20240506224600950

案例一中获取注解对象的做法是下面这样的:

image-20240504112116994

分析:先抽取表达式,即那个切点表达式+注解,表达式写在随便一个方法上,方法的方法体没有意义,所以不写,写了也没有用。然后增强的内容上使用那个抽取的表达式。这样的效果就是在目标方法执行前,先进行增强,这里是前置增强(看@Before注解里面写的方法,代表上面的被抽取的表达式,这个表达式就是用于确定增强方法的。还有一点需要注意:AOP增强内容的这些方法,参数里面都可以直接写JoinPoint的,如果你要用到目标方法的一些东西,那么就直接写这个参数,然后去使用它的方法调用原方法就行了)。上面前置增强的效果是,在执行目标方法之前,会先执行autoFill(JoinPoint joinPoint)方法,autoFill(JoinPoint joinPoint)这个方法里面我们通过joinPoint拿到了方法上面的注解,然后利于注解中的值来进行对应的增强。注意,因为前置增强不需要调用目标方法也会自动去执行原来的目标方法。所以这里不用使用joinPoint调用原方法。

上面这种既展示了普通的切点表达式来进行匹配,也展示了普通的注解的方式来进行匹配方法,用了&&符号来让两个条件都满足才进行增强。因为@Pointcut也是支持SpringEL表达式的写法的,所以值写EL表达式也行,因为内部的解析程序考虑到了EL表达式的写法。

下面要展示的是另一种注解匹配的写法,也一样可以获取到注解中的内容。

相当于传了一个注解过来,然后注解中带着一些数据。

image-20240504112403452

注意:这种写法的话,要求参数名和@annotation注解中的名字要一样才行(如果需要JoinPoint,那么就要把JoinPoint写在方法的第一个参数才行)。比如下面这样:

image-20240504113730861

这种写法,就是,不用抽取。直接把注解写在方法参数里面,然后@annotation内写方法参数中的这个注解参数的参数名。这种写法spring能自动知道,要去切这个注解对应的方法。

上面的写法就相当于是下面这样的写法(下面这个写法和上面写法的执行结果是一样的,但是如果要获取注解中的数据,把注解写在方法形参里面的写法使用起来更加顺滑一点。):

image-20240506230857938

案例引入

若依中是怎么来实现数据权限的?

我们先来看数据权限使用的地方:

controller层:

image-20240509230500029

service接口,只有方法声明(没什么特殊的):

image-20240509230527539

然后就是service层的实现了,实现的方法上面有一个注解。

(总之就是,注解的作用就相当于是把一些值放到了注解里面,然后注解挂在方法上面了而已,然后解析程序去拿这个数据而已,如果没有解析注解的程序,那么这个注解将不会有任何作用,只是挂一个信息到方法上而已。注解和注释的区别就是:注解是给程序看的,程序可以看到并使用这个信息,当然不使用也行。但是注释是给开发者看的,程序是看不到注释的信息的。注解的作用,其实就是挂一个信息到某个方法或者某个类上而已,到时候解析程序可以通过反射去拿到注解,并且获取到注解里面的数据,然后进行对应的解析操作。):

image-20240509230603115

然后就是mapper接口(没有什么特殊的):

image-20240509230824463

然后发现xml中有一个${params.dataScope}:

image-20240509230900140

通过我们对xml中写占位符的经验,我们知道KaTeX parse error: Expected 'EOF', got '#' at position 53: …sql注入问题。那么为什么不用#̲{}呢?因为这个虽然不会sql…{},但是注意使用这个的话,我们代码中要注意排除sql注入问题。

因为,mapper层的方法长这样:

image-20240509233416501

所以,我们可以确定${params.dataScope}表示的是,取dept对象中的params属性的dataScope属性值。

我们找了SysDept找不到params变量

image-20240509233531684

但是,我们在父类中可以看到params变量了:

image-20240509233642378

image-20240509233910826

image-20240523235942107

params.dataScope其实不止是取某个对象的某个属性,他也可以表示取“属性名叫params的map中键为dataScope的值”。

所以是OK的。

然后我们看看一个奇怪的现象:

我们看看请求:

image-20240509234311577

我们看到执行的sql竟然被增强了,即params.dataScope被给了值

image-20240509234414280

但是这个值是怎么来的呢?

答:AOP切入的。

课外知识点:java中 ${} 和 #{} 有什么区别

前言

${} 和 #{} 都是 MyBatis 中用来替换参数的,它们都可以将用户传递过来的参数,替换到 MyBatis 最终生成的 SQL 中,但它们区别却是很大的,接下来我们一起来看。

1.功能不同

${} 是将参数直接替换到 SQL 中比如以下代码:

<select id="getUserById" resultType="com.example.demo.model.UserInfo">
 select * from userinfo where id=${id}
</select>

最终生成的执行 SQL 如下:

g

从上图可以看出,之前的参数 ${id} 被直接替换成具体的参数值 1 了。

#{} 则是使用占位符的方式,用预处理的方式来执行业务。比如,我们将上面的案例改造为 #{} 的形式,实现代码如下:

<select id="getUserById" resultType="com.example.demo.model.UserInfo">
 select * from userinfo where id=#{id}
</select>

最终生成的 SQL 如下:

img

即,有问号的。

区别

比如下面代码:

<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
 select * from userinfo where name=${name}
</select>

以上程序执行时,生成的 SQL 语句如下:

img

这样就会导致程序报错,因为传递的参数是字符类型的,而在 SQL 的语法中,如果是字符类型需要给值添加单引号,否则就会报错,而 ${} 是直接替换,不会自动添加单引号,所以执行就报错了。前面的例子里面,没有加单引号也能正确,是因为,填充的是数值,数值在sql中不需要加什么引号,直接把值写上去就行了。

上面出错的代码使用 #{} 来做就可以成功执行,因为 #{} 采用的是占位符预执行的,所以不存在任何问题,它的实现代码如下:

比如:

<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
 select * from userinfo where name=#{name}
</select>

以上程序最终生成的执行 SQL 如下:

img

但是在我们需要传一个sql关键字给xml中的sql的时候,占位符就做不到了。

比如,当我们要根据价格从高到低(倒序)、或从低到高(正序)查询,并且排序可以切换时,如下图所示:

img

这时我们就需要传递排序的关键字了。即,要传sql中的一些sql字符,而不是只传值的时候,我们可以使用 ${} 来实现:

比如:

<select id="getAll" resultType="com.example.demo.model.Goods">
select * from goods order by price ${sort}
</select>

以上代码生成的执行 SQL 和运行结果如下:

image.png

但是,如果将代码中的 ${} 改为 #{},那么程序执行就会报错,#{} 的实现代码如下:

<select id="getAll" resultType="com.example.demo.model.Goods">
select * from goods order by price #{sort}
</select>

以上代码生成的执行 SQL 和运行结果如下:

img

总结

**从上述的执行结果我们可以看出:**当传递的是普通参数时,需要使用 #{} 的方式,而当传递的是 SQL 命令或 SQL 关键字时,需要使用 ${} 来对 SQL 中的参数进行直接替换并执行。

除此之外,${} 和 #{} 最主要的区别体现在安全方面,当使用 ${} 会出现安全问题,也就是 SQL 注入的问题,而使用 #{} 因为是预处理的,所以不会存在安全问题。这个例子就不举了,之前学习的时候学过。

若依AOP如何做到数据权限

内容1

下面我们来探究一下若依AOP如何做到数据权限的。

点击查询

image-20240514220956450

控制层方法看到param中还没有数据:

image-20240514221019044

在service层中就看到了数据:

image-20240514221143534

说明在这个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.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值