篇一

基于框架中使用的MyBatis-Plus整理了两个方法,一种是MyBatis-Plus自带的TypeHandler,另一种是基于MyBatis的Intercept拦截器。方法一配置简单使用麻烦,方法二配置麻烦使用简单,自己斟酌使用即可。当然方法二适用只使用MyBatis + Springboot的架构。
MyBatis-Plus版本:3.4.0

一、基于MyBatis-Plus自定义类型处理器(TypeHandler)的方法

1、创建TypeHandler

//AES 是工具方法类,按加密需求设置
public class AESEncryptHandler extends BaseTypeHandler {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, AES.encrypt((String)parameter));
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String columnValue = rs.getString(columnName);
        return AES.decrypt(columnValue);
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String columnValue = rs.getString(columnIndex);
        return AES.decrypt(columnValue);
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        String columnValue = cs.getString(columnIndex);
        return AES.decrypt(columnValue);
    }
}

2、开启MyBatis-Plus的扫描

在配置文件中加入配置

mybatis-plus.type-handlers-package=com.demo.mybatisplusintercept.intercept

配置说明(来源官方文档):

mybatis aes加密 mybatis字段加密解密_spring boot


PS:实测不加这个配置也可以使用,未深入,原因不明

3、实体类中加入注解

类上加入注解

[@TableName(autoResultMap ](/TableName(autoResultMap ) = true)

字段上加入注解

[@TableField(typeHandler ](/TableField(typeHandler ) = AESEncryptHandler.class)

mybatis aes加密 mybatis字段加密解密_spring boot_02

4、查询中指定TypeHandler

使用MyBatis-Plus默认方法查询时无需指定,但是当需要使用sql查询的时候,需要指定TypeHandler

注解

mybatis aes加密 mybatis字段加密解密_mybatis aes加密_03

XML

mybatis aes加密 mybatis字段加密解密_mybatis aes加密_04

二、基于MyBatis的方法(拦截器)

mybatis aes加密 mybatis字段加密解密_spring boot_05


在官方示例(https://mybatis.org/mybatis-3/zh/configuration.html#plugins)中,是直接监听Executor,但是通过文章介绍可以看到,ParameterHandler和ResultSetHandler分别操作入参和查询,所以在此处操作字段的加解密应该更为合理。

1、自定义注解

类注解

@Documented
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptClass{

}

字段注解

@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptField {

}

2、工具类

package com.demo.mybatisplusintercept.intercept;

import com.demo.mybatisplusintercept.annotation.EncryptDecryptField;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Objects;

/**
 * Domain数据加密工具类
 *
 */
public class EncryptDecryptUtils {

	/**
	 * 多field加密方法
	 *
	 * @param declaredFields
	 * @param parameterObject
	 * @param <T>
	 * @return
	 * @throws IllegalAccessException
	 */
	public static <T> T encrypt(Field[] declaredFields, T parameterObject) throws IllegalAccessException {
		for (Field field : declaredFields) {
			EncryptDecryptField annotation = field.getAnnotation(EncryptDecryptField.class);
			if (Objects.isNull(annotation)) {
				continue;
			}
			encrypt(field, parameterObject);
		}
		return parameterObject;
	}


	/**
	 * 单个field加密方法
	 *
	 * @param field
	 * @param parameterObject
	 * @param <T>
	 * @return
	 * @throws IllegalAccessException
	 */
	public static <T> T encrypt(Field field, T parameterObject) throws IllegalAccessException {
		field.setAccessible(true);
		Object object = field.get(parameterObject);
		if (object instanceof BigDecimal) {
			BigDecimal value = (BigDecimal) object;
			long longValue = value.movePointRight(4).subtract(BigDecimal.valueOf(Integer.MAX_VALUE >> 3)).longValue();
			field.set(parameterObject, BigDecimal.valueOf(longValue));
		} else if (object instanceof Integer) {

		} else if (object instanceof Long) {

		} else if (object instanceof String) {
			//定制String类型的加密算法
			String value = (String) object;
			field.set(parameterObject, AES.encrypt(value));
		}
		return parameterObject;
	}

	/**
	 * 解密方法
	 *
	 * @param result
	 * @param <T>
	 * @return
	 * @throws IllegalAccessException
	 */
	public static <T> T decrypt(T result) throws IllegalAccessException {
		Class<?> parameterObjectClass = result.getClass();
		Field[] declaredFields = parameterObjectClass.getDeclaredFields();
		decrypt(declaredFields, result);
		return result;
	}

	/**
	 * 多个field解密方法
	 *
	 * @param declaredFields
	 * @param result
	 * @throws IllegalAccessException
	 */
	public static void decrypt(Field[] declaredFields, Object result) throws IllegalAccessException {
		for (Field field : declaredFields) {
			EncryptDecryptField annotation = field.getAnnotation(EncryptDecryptField.class);
			if (Objects.isNull(annotation)) {
				continue;
			}
			decrypt(field, result);
		}
	}

	/**
	 * 单个field解密方法
	 *
	 * @param field
	 * @param result
	 * @throws IllegalAccessException
	 */
	public static void decrypt(Field field, Object result) throws IllegalAccessException {
		field.setAccessible(true);
		Object object = field.get(result);
		if (object instanceof BigDecimal) {
			BigDecimal value = (BigDecimal) object;
			double doubleValue = value.add(BigDecimal.valueOf(Integer.MAX_VALUE >> 3)).movePointLeft(4).doubleValue();
			field.set(result, BigDecimal.valueOf(doubleValue));
		} else if (object instanceof Integer) {

		} else if (object instanceof Long) {

		} else if (object instanceof String) {
			//定制String类型的解密算法
			String value = (String) object;
			field.set(result, AES.decrypt(value));

		}
	}


}

3、两个拦截器

@Intercepts({
        @Signature(type = ParameterHandler.class,method = "setParameters",args = PreparedStatement.class)
})
@ConditionalOnProperty(value = "domain.encrypt",havingValue = "true")
@Slf4j
public class MybatisParameterIntercept implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        //拦截 ParameterHandler 的 setParameters 方法 动态设置参数
        if (invocation.getTarget() instanceof ParameterHandler) {
            ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();

            // 反射获取 参数对象
            Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
            parameterField.setAccessible(true);
            Object parameterObject = parameterField.get(parameterHandler);
            if (Objects.nonNull(parameterObject)){
                Class<?> parameterObjectClass = parameterObject.getClass();
                EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(parameterObjectClass, EncryptDecryptClass.class);
                if (Objects.nonNull(encryptDecryptClass)){
                    Field[] declaredFields = parameterObjectClass.getDeclaredFields();

                    final Object encrypt = EncryptDecryptUtils.encrypt(declaredFields, parameterObject);
                }
            }
        }
        return invocation.proceed();
    }
}
@Intercepts({
        @Signature(type = ResultSetHandler.class,method = "handleResultSets",args = Statement.class)
})
@ConditionalOnProperty(value = "domain.encrypt",havingValue = "true")
@Component
@Slf4j
public class MybatisResultSetIntercept implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (Objects.isNull(result)){
            return null;
        }

        if (result instanceof ArrayList) {
            ArrayList resultList = (ArrayList) result;
            if (CollectionUtils.isNotEmpty(resultList) && needToDecrypt(resultList.get(0))){
                for (int i = 0; i < resultList.size(); i++) {
                    EncryptDecryptUtils.decrypt(resultList.get(i));
                }
            }
        }else {
            if (needToDecrypt(result)){
                EncryptDecryptUtils.decrypt(result);
            }
        }
        return result;
    }

    private boolean needToDecrypt(Object object){
        Class<?> objectClass = object.getClass();
        EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(objectClass, EncryptDecryptClass.class);
        if (Objects.nonNull(encryptDecryptClass)){
            return true;
        }
        return false;
    }
}

4、注册拦截器

/**
 * 注册mybatis拦截器
 */
@Configuration
@MapperScan("com.demo.mybatisplusintercept.dao")
public class MyBatisPlusConfig {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 注册MyBatis拦截器
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    public String myInterceptor(SqlSessionFactory sqlSessionFactory){

        sqlSessionFactory.getConfiguration().addInterceptor(parameterIntercept());

        sqlSessionFactory.getConfiguration().addInterceptor(resultSetIntercept());

        return "myInterceptor";
    }

    public MybatisParameterIntercept parameterIntercept(){
        return applicationContext.getAutowireCapableBeanFactory().createBean(MybatisParameterIntercept.class);
    }

    public MybatisResultSetIntercept resultSetIntercept(){
        return applicationContext.getAutowireCapableBeanFactory().createBean(MybatisResultSetIntercept.class);
    }

}

5、实体类添加注解

@Data
@TableName(value = "user",autoResultMap = true)
@EncryptDecryptClass
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

//    @TableField(typeHandler = AESEncryptHandler.class)
    @EncryptDecryptField
    private String mobile;

//    @TableField(typeHandler = AESEncryptHandler.class)
    @EncryptDecryptField
    private String idcard;

}

篇二、修复Mybatis-Plus自带方法不兼容问题

不生效原因

首先准备两个update方法进行对比,第一个update方法中**this**指代的是继承框架方法的**ServiceImpl**,第二个update2方法中直接走到数据交互层操作sql去了。

mybatis aes加密 mybatis字段加密解密_mybatis_06

调用第一个**update**

mybatis aes加密 mybatis字段加密解密_数据库_07

可以看到框架的方法进来的时候会出现两个入参,并且神奇的是堆栈地址是同一个

调用第二个**update2**

mybatis aes加密 mybatis字段加密解密_mybatis_08

可以看到parameterObject对象直接就是入参对象。

写到这的时候,突然想到是不是使用@Param注解修饰入参也会这样,尝试了一下,果然:

mybatis aes加密 mybatis字段加密解密_spring boot_09

在篇一的代码中,是直接从parameterObject对象中获取字段是否存在EncryptDecryptClass.class注解,很显然没有,然后在随后的if判断中就直接略过,这边优化一下就行了。

mybatis aes加密 mybatis字段加密解密_spring boot_10

优化

先放代码:

...
public class MybatisParameterIntercept implements Interceptor {

    ...

	//获取参数对象方法名称
    private final static String getParameterObjectMethod = "getParameterObject";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        //拦截 ParameterHandler 的 setParameters 方法 动态设置参数
        if (invocation.getTarget() instanceof ParameterHandler) {
            ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();

            // 反射获取 参数方法
            Class<? extends ParameterHandler> aClass = parameterHandler.getClass();
            Method getParameterObject = aClass.getDeclaredMethod(getParameterObjectMethod);
            Object parameterObject = getParameterObject.invoke(parameterHandler);

            if (Objects.nonNull(parameterObject)){
                if (parameterObject instanceof MapperMethod.ParamMap){  //mybatis-plus 框架默认多参数
                    MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject;

                    //避免相同的key重复执行,使用set对象记录堆栈地址
                    HashSet<Integer> stackSet = new HashSet<>();

                    for (Object key : paramMap.keySet()) {
                        //需要使用hashcode判断,equals()方法可能被重写。
                        if (stackSet.add(System.identityHashCode(paramMap.get(key)))) {
                            EncryptDecryptUtils.doEncrypt(paramMap.get(key));
                        }
                    }
                } else {
                    EncryptDecryptUtils.doEncrypt(parameterObject);
                }
            }
        }
        return invocation.proceed();
    }

    ...
    
}

变动的点:

  1. 获取parameterObject的路子由直接获取属性到调用对象上的getParameterObject方法获取属性。解释一下,实际开发的时候因为版本跟demo不一样,所以官方把属性封装起来了,只能通过getParameterObject方法获取值。
  2. 增加判断**parameterObject instanceof MapperMethod.ParamMap**。初步观察parameterObject对象的类别为MapperMethod.ParamMap,直接判断进入不同的支线。获取到的ParamMap很明显是个Map结构对象啦。
  3. 从之前的截图中发现两个对象的堆栈地址是一致的,所以用stackSet对对象进行去重,防止重复加密,会加密两次哦。
  4. 原本进行加密的代码抽离到了doEncrypt方法中。