慢慢渗透Spring AOP概念(二) --- 代码演示

在上一篇文章中已经将SpringAOP的有关注解以及代理模式做了一些介绍和理解,本篇文章指在为了更好在项目中使用运用切面AOP的思想

一: 操作日志

我们在项目中可能涉及保存用户的操作记录,来达到监控项目的作用,这个时候如果我们每次都把业务逻辑和对系统的操作记录都放在一个方法中就是会造成项目的臃肿。不利于维护,性能也会变得很差。所以我们可以使用我们AOP切面编程的思想来处理这个问题。
这边我们采用自定义注解+SpringAOP的方式 记录用户对系统的操作:
1: 定义日志注解

package com.hwrest.jeadmin.common.annotation;

import java.lang.annotation.*;

/**
 * @author queiter.ex
 * @title: SysLog
 * @projectName 
 * @description: 日志注解
 * @date 2019/12/309:13
 */
@Documented
@Target(ElementType.METHOD) // 该注解放在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {

    String name() default "";
}

@Documented,@Target,@Retention 这三个元注解的代表的含义
2:定义切面处理类
切面处理类主要处理@SysLog 注解到什么方法上处理该方法的逻辑

package com.hwrest.jeadmin.common.aspect;

import com.google.gson.Gson;
import com.hwrest.jeadmin.common.annotation.SysLog;
import com.hwrest.jeadmin.common.utils.HttpContextUtils;
import com.hwrest.jeadmin.common.utils.IPUtils;
import com.hwrest.jeadmin.common.utils.ShiroUtils;
import com.hwrest.jeadmin.modules.sys.entity.SysLogEntity;
import com.hwrest.jeadmin.modules.sys.entity.SysUserEntity;
import com.hwrest.jeadmin.modules.sys.service.SysLogService;
import org.apache.shiro.subject.Subject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;

/**
 * @author queiter.ex
 * @title: SysLogAspect
 * @projectName je_admin
 * @description: 日志切面处理类
 * @date 2019/12/309:40
 */
@Component
@Aspect
public class SysLogAspect {

    // 注入操作日志的接口
    @Autowired
    private SysLogService sysLogService;


    // 定义切点
    @Pointcut("@annotation(com.hwrest.jeadmin.common.annotation.SysLog)")
    public void logPointCut(){

    }


    // 从在项目中方法凡是有注解的地方获取方法的参数和注解注释的内容
    @Around("logPointCut()") // 环绕通知
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 执行开始的时间
        long begTime = System.currentTimeMillis();
        // 执行的方法
        Object proceed = point.proceed();
        // 执行的时间
        long time = System.currentTimeMillis() - begTime;
        // 保存日志
        saveLog(point, time);
        return proceed;
    }
     /**
      * @Author liuhongwei
      * @Description 保存日志的信息
      * @Date 10:05 2019/12/30
      * @param
      * @return
      */
    private void saveLog(ProceedingJoinPoint point, long time) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 拿到 方法
        Method method = signature.getMethod();

        SysLogEntity sysLogEntity = new SysLogEntity();
        // 通过方法拿到注解的方法
        SysLog annotation = method.getAnnotation(SysLog.class);
        // 当注解存在的时候  防止空指针异常
        if (annotation != null) {
            // annotation.name() 获取注解上该方法的文字
            sysLogEntity.setOperation(annotation.name());
        }
        // 获取类的名称   全路径的名称
        String className = point.getTarget().getClass().getName();
        // 获取方法的名字
        String methodName = signature.getName();
        // 将 类名和方法名字组装一下就是方法的全路径名称
        sysLogEntity.setMethod(className + "." + methodName + "()");

        // 获取请求的参数

        Object[] args = point.getArgs();

        try {
            String params = new Gson().toJson(args);
            sysLogEntity.setParams(params);
        } catch (Exception e) {

        }
        // 获取请求的Request
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();

        // 根据 request 获取用户的ip地址
        sysLogEntity.setIp(IPUtils.getIpAddr(request));

        // 设置用户名称
        SysUserEntity sysUserEntity = (SysUserEntity) ShiroUtils.getSubject();

        sysLogEntity.setUsername(sysLogEntity.getUsername());

        // 设置时间请求的时间
        sysLogEntity.setTime(time);

        sysLogEntity.setCreateDate(new Date());
        // 保存日志
        sysLogService.save(sysLogEntity);
    }
}

SysLogEntity 实体类

package com.hwrest.jeadmin.modules.sys.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @author queiter.ex
 * @title: SysLogEntity
 * @projectName je_admin
 * @description: TODO
 * @date 2019/12/309:43
 */
@Data
@TableName(value = "sys_log")
public class SysLogEntity implements Serializable {

    @TableId
    private Long id;
    //用户名
    private String username;
    //用户操作
    private String operation;
    //请求方法
    private String method;
    //请求参数
    private String params;
    //执行时长(毫秒)
    private Long time;
    //IP地址
    private String ip;
    //创建时间
    private Date createDate;

}

上述就是关于我们记录操作日志的代码实现,我们主要利用了自定义注解注解到哪个方法上,然后采用切面处理的方式获取被注解的方法方法名称和参数的名称。存到我们数据库的操作日志中

二:数据权限

我们在项目中,我们会涉及到数据权限问题。在数据权限上我们一般是通过sql语句进行数据的限制,假如,现在项目中有很多的模块都涉及到数据权限的问题,那么我们需要在每个模块响应的sql上加上数据权限相关的sql语句,如果这样实现的话我们就会造成在Mapper.xm中sql的语句非常多。代码看起来比较臃肿而且还不易于维护。所以我们为了简化代码,我们采用注解的方式通过切面类进行组装统一的数据权限的sql。
1:定义数据权限的注解

package com.hwrest.jeadmin.common.annotation;

import java.lang.annotation.*;

/**
 * @author quiter.ex
 * @title: DataFilter
 * @projectName je_admin
 * @description: 数据权限过滤注解
 * @date 2019/12/3014:02
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataFilter {

    /**
     * 表的别名
     */
    String tableAlias() default "";

    /**
     * 查询条件前缀,可选值有:[where、and]
     */
    String prefix() default "";

    /**
     * 用户ID
     */
    String userId() default "creator";

    /**
     * 部门ID
     */
    String deptId() default "dept_id";

}

2:定义切面类,获取注解定义的参数,并组装数据

package com.hwrest.jeadmin.common.aspect;

import cn.hutool.core.collection.CollUtil;
import com.hwrest.jeadmin.common.annotation.DataFilter;
import com.hwrest.jeadmin.common.enumAdmin.SuperAdminEnum;
import com.hwrest.jeadmin.common.utils.ShiroUtils;
import com.hwrest.jeadmin.modules.sys.entity.SysUserEntity;
import org.apache.commons.lang.StringUtils;
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.util.List;
import java.util.Map;

/**
 * @author liuhongwei.ex
 * @title: DataFilterAspect
 * @projectName je_admin
 * @description: 数据权限处理类
 * @date 2019/12/3014:10
 */
@Aspect // 定义此类是一个切面处理类
@Component // 注册Bean容器当中去
public class DataFilterAspect {

    // 定义切点
    @Pointcut("@annotation(com.hwrest.jeadmin.common.annotation.DataFilter)")
    public void dataFilterCut() {

    }

    // 处理切点从注解中获取到的字段组装sql语句
    @Before("dataFilterCut()")
    public void dataFilter(JoinPoint point) {
        // 获取注解标注的方法参数是否为存在
        Object params = point.getArgs()[0];

        if (params != null && params instanceof Map) {
           SysUserEntity sysUserEntity = (SysUserEntity) ShiroUtils.getSubject();

           // 该用户是否为管理员
           if (sysUserEntity.getIsAdmin() == SuperAdminEnum.NO.value()) {
  Map map = (Map)params;
                String sqlFilter = getSqlFilter(user, point);
                map.put("sqlFilter", new DataScope(sqlFilter));
           }


        }
    }

    public String getSqlFilter(JoinPoint point, SysUserEntity sysUserEntity) {
        // 获取动态的连接点
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 获取注解权限的 注解
        DataFilter annotation = signature.getMethod().getAnnotation(DataFilter.class);
        // 获取表的别名
        String tableAlias = annotation.tableAlias();
        if (StringUtils.isNotBlank(tableAlias)) {
            tableAlias += ".";
        }

        StringBuilder stringBuilder = new StringBuilder();

        // 获取查询条件的前缀
        String prefix = annotation.prefix();
        // 组装数据结构
        if (StringUtils.isNotBlank(prefix)) {
            stringBuilder.append(" ").append(prefix);
        }
        stringBuilder.append(" (");

        // 获取部门的列表
        List<Long> roleIdList = sysUserEntity.getDeptIdList();
        if (CollUtil.isNotEmpty(roleIdList)) {
            stringBuilder.append(tableAlias).append(annotation.deptId());


            stringBuilder.append(" in(").append(StringUtils.join(roleIdList, ",")).append(") ");
        }

        // 查询本人的数据
        if (CollUtil.isNotEmpty(roleIdList)) {
            stringBuilder.append(" or ");
        }

        stringBuilder.append(tableAlias).append(annotation.userId()).append(" = ").append(sysUserEntity.getUserId());

        stringBuilder.append("(");

        return stringBuilder.toString();

    }
}

3:这边因为是采用了MybatisPlus组件,所以我们可以使用拦截器(再生成sql语句的时候)拦截住sql然后重新拼装sql语句。

package com.hwrest.jeadmin.common.interceptor;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.hwrest.jeadmin.modules.sys.entity.DataScope;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

/**
 * @author quiter.ex
 * @title: DataFilterInterceptor
 * @projectName je_admin
 * @description: TODO
 * @date 2019/12/3016:04
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataFilterInterceptor extends AbstractSqlParserHandler implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());

        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // 解析sql
        this.sqlParser(metaObject);

        // 判断一下是不是SELECT的操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        //针对定义了rowBounds, 作为mapper接口方法的参数
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.mappedStatement");
        // 拿到sql并转化成大写
        String originalSql = boundSql.getSql().toUpperCase();
        // 拿到sql的参数
        Object parameterObject = boundSql.getParameterObject();

        // 判断参数里是否有DataScope的对象
        DataScope scope = null;

        if (parameterObject instanceof DataScope) {
            scope = (DataScope) parameterObject;
        } else if (parameterObject instanceof Map) {
            for (Object args : ((Map) parameterObject).values())
                if (args instanceof DataScope) {
                    scope = (DataScope) args;
                    break;
                }
        }

        // 当不用数据过滤的时候

        if (scope == null) {
            return invocation.proceed();
        }


        // 拼接新的sql
        if (originalSql.indexOf(" ORDER BY ") != -1) {
            originalSql = originalSql.replace("ORDER BY", scope.getSqlFilter() + " ORDER BY ");
        } else {
            originalSql = originalSql + scope.getSqlFilter();
        }

        // 重写这个sql

        metaObject.setValue("delegate.boundSql.sql", originalSql);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

上述两个就是利用SpringAop的思想概念来简化代码的操作如有错误欢迎大家前来指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值