【Mybatis】Mybatis拦截器+注解,实现敏感数据自动加解密

一:背景

         今天,公司新要求对数据库的敏感数据进行加密,对手机号、身份证号、姓名等一些敏感数据进行加密处理,要求:

        1. 通过程序实现数据加密解密 (快速便捷,尽量减少对原先代码的修改)

        2. 可以通过sql查询对数据进行解密(通过密钥直接对数据库加密参数进行解密)

        3. 数据库:mysql

二:思考

        1. 需要用到sql对数据库进行直接解密,同时又比较安全,第一个想到的是使用mysql自带的AES对数据进行加密

        2. 对代码减少修改,想到使用mybatis拦截器实现,对 sql语句 或者 参数 进行修改

        3. 数据只需要在入库层进行加解密,上层业务调用不需要再考虑敏感数据的加解密问题

三:mysql AES加解密

-- 加密 key: 123213  val: qwertyuiop123

select to_base64(AES_ENCRYPT('qwertyuiop123','123213'))

-- 返回:ZDuEEEeORjQFUlr7rkTsQg==



-- 解密:key: ZDuEEEeORjQFUlr7rkTsQg==  val: qwertyuiop123

SELECT CONVERT(AES_DECRYPT(from_base64('ZDuEEEeORjQFUlr7rkTsQg=='), '123213') USING UTF8MB4)

-- 返回:qwertyuiop123

四:实现一: 修改sql语句实现入库加密

        实现方式:

@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class InsuranceMybatisPlugin implements Interceptor {

    private String myBatisAESKey = "123123";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            // 获取要执行的 sql
            BoundSql boundSql = statementHandler.getBoundSql();
            String sqlBefore = boundSql.getSql();

            // 判断当前sql 是否需要加密
            boolean containsChannel = sqlBefore.contains("{{myBatisAESKey}}");

            // 替换加密需要的key
            if(StringUtils.isNotBlank(sqlBefore) && containsChannel){
                
                // 替换key值    
                String sqlAfter = sqlBefore.replaceAll("\\{\\{myBatisAESKey\\}\\}", myBatisAESKey );
                Field sqlField = boundSql.getClass().getDeclaredField("sql");
                
                // 重新设置对应sql
                sqlField.setAccessible(true);
                ReflectionUtils.setField(sqlField, boundSql, sqlAfter);
                sqlField.setAccessible(false);
            }
        }catch (Exception e){
            // 打印错误日志
        }
        return invocation.proceed();
    }

}
-- xml sql加密修改 
select 加密字段

-- 改为:
select SELECT to_base64(AES_ENCRYPT(加密字段,'{{myBatisAESKey}}'))


-- xml sql 解密修改
select 解密字段

--改为:
select convert(AES_DECRYPT(from_base64(解密字段), '{{myBatisAESKey}}') using UTF8MB4)

优点:

        1. 实现简单,只需要不到20行代码就能实现

        2. 可以使用mysql自带的解密

        3. 效率比较高,一般sql语句都不会太长,替换比较快

缺点:

        1. 需要修改原先写好的xml sql, 如果项目比较大的话工作量也不怎么轻松

        2. 后续如果换数据库的话,又需要重新更改sql

        3. 系统需要重新测试,防止人为因素导致sql修改错误(粗心导致的sql异常)

五:实现二:mybatis拦截器 + 注解

        1. 需要解决的问题:

                1. 程序加密后 可以使用 mysql 自带的函数进行解密

                2. 不修改原先sql, 就能实现加密

                3. 降低对程序的影响,减少代码侵入

        2. 解决加解密问题

              mysql 使用AES加密,程序我也可以用同样的加解密方式,这不就没问题了,代码实现:

public class EncryptUtil {

    private static final Logger log = LoggerFactory.getLogger(EncryptUtil.class);

    /**
     * 加密算法
     */
    private static final String KEY_ALGORITHM = "AES";


    /**
     * 算法/模式/补码方式
     */
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";


    /**
     * 编码格式
     */
    private static final String CODE = "utf-8";


    /**
     * base64验证规则
     */
    private static final String BASE64_RULE = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)=?$";


    /**
     * 正则验证对象
     */
    private static final Pattern PATTERN = Pattern.compile(BASE64_RULE);



    /**
     * 加密
     * @param content 加密参数
     * @param key     加密key
     * @return 结果字符串
     */
    public static String encrypt(String content, String key) {
        // 判断如果已经是base64加密字符串则返回原字符串
        if (Objects.isNull(content) || isBase64(content)) {
            return content;
        }


        byte[] encrypted = encrypt2bytes(content, key);
        if (null == encrypted || encrypted.length < 1) {
            return null;
        }

        // 转成 base64
        return Base64Utils.encodeToString(encrypted);

    }


    /**
     * @param content 加密字符串
     * @param key     加密key
     * @return 返回加密字节
     */
    public static byte[] encrypt2bytes(String content, String key) {
        try {
            byte[] raw = key.getBytes(CODE);
            SecretKeySpec secretKeySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
            return cipher.doFinal(content.getBytes(CODE));
        } catch (Exception e) {
            log.error("encrypt2bytes to content: {}, key {}", content, key, e);
            return null;
        }
    }



    /**
     * 解密
     *
     * @param content 解密字符串
     * @param key     解密key
     * @return 解密结果
     */
    public static String decrypt(String content, String key) {
        // 不是base64格式字符串则不进行解密
        if (!isBase64(content)) {
            return content;
        }

        // 先转成 Base64 再进行解密
        return decrypt(Base64Utils.decodeFromString(content), key);
    }



    /**
     * @param content 解密字节
     * @param key     解密key
     * @return 返回解密内容
     */
    public static String decrypt(byte[] content, String key) {
        if (key == null) {
            return null;
        }

        try {
            byte[] raw = key.getBytes(CODE);
            SecretKeySpec keySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] original = cipher.doFinal(content);
            return new String(original, CODE);
        } catch (Exception e) {
            log.error("failed to decrypt content: {} key: {}", content, key, e);
            return null;
        }
    }



    /**
     * 判断是否为 base64加密
     *
     * @param str 参数
     * @return 结果
     */
    public static boolean isBase64(String str) {
        Matcher matcher = PATTERN.matcher(str);
        return matcher.matches();
    }



    /**
     * 加密 解密实验
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        System.out.println(EncryptUtil.encrypt("138094asdas49025", "qwertyuieqweqw12"));
        System.out.println(EncryptUtil.decrypt("BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=", "qwertyuieqweqw12"));

       // 执行结果为:
       // 加密: BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=
       // 解密: 138094asdas49025
    }

mysql 加解密:

select to_base64(AES_ENCRYPT('138094asdas49025','qwertyuieqweqw12'))
select convert(AES_DECRYPT(from_base64('BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY='), 'qwertyuieqweqw12') using UTF8MB4)

-- 执行结果
-- 加密:BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=
-- 解密:138094asdas49025

        3. 实现参数加密,入参加密:

                方法标识SensitiveMethodParam 注解
/**
 * 方法标识,入参加密
 */
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMethodParam {

}
        参数标识SensitiveParam注解,具体加密的字段
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveParam {
}
        拦截器实现:
@Component
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class BarExecuteInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(BarExecuteInterceptor.class);

    private final String AES_KEY = "qwertyuiopasdfgh";

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

        // 获取当前方法
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

        // 方法为空, 正常执行
        Method interfaceMethod = InterceptorUtil.getInterfaceMethod(mappedStatement.getId());
        Object parameterObject = invocation.getArgs()[1];

        // 一个参数的情况下 直接放入当前参数,多个参数的情况下 ParamMap 将结果进行包装
        // 存在 param 注解 key 为 param
        // 验证是否存在加密参数方法
        if (Objects.isNull(interfaceMethod) || Objects.isNull(parameterObject) || isNotSensitiveMethodParam(interfaceMethod))
            return invocation.proceed();

        // 获取加密的参数名称,多个参数
        Set<String> sensitiveNameSet = getSensitiveNameSet(interfaceMethod);

        // 多个参数 mybatis 使用 MapperMethod.ParamMap进行包装
        sensitiveParam(invocation, sensitiveNameSet, true);

        // 重新生成动态参数, list<Sting>集合的动态参数会被解析出来,所以需要重新生成一下参数
        setBoundSql(mappedStatement, invocation.getArgs());

        // 执行完成后将参数进行解密,防止影响后续流程,
        Object result;
        try {
            result =  invocation.proceed();
        } finally {
            sensitiveParam(invocation, sensitiveNameSet, false);
        }

        // 返回执行结果
        return result;
    }



    /**
     * 验证当前方法是否为加密参数方法
     *
     * @param interfaceMethod MappedStatement 对应方法
     * @return
     */
    private boolean isNotSensitiveMethodParam(Method interfaceMethod) {
        SensitiveMethodParam annotation = interfaceMethod.getAnnotation(SensitiveMethodParam.class);
        return Objects.isNull(annotation);
    }



    /**
     * 获取方法中添加了注解的方法
     *
     * @param method
     * @return
     */
    private Set<String> getSensitiveNameSet(Method method) {
        Set<String> sensitiveNames = new HashSet<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            Annotation[] annotations = method.getParameterAnnotations()[i];

            // 验证参数 是否存在 SensitiveMethodParam 注解
            boolean noneMatch = Arrays.stream(annotations)
                    .noneMatch(annotation -> annotation instanceof SensitiveParam);
            if (noneMatch) continue;

            // 获取 paramMap 的 key, Param注解 > 形参名称
            String name = Arrays.stream(annotations)
                    .filter(annotation -> annotation instanceof Param)
                    .map(annotation -> ((Param) annotation).value())
                    .findFirst()
                    .orElse(parameter.getName());

            sensitiveNames.add(name);
        }

        return sensitiveNames;
    }


    /**
     * 加解密参数
     * @param invocation 对象
     * @param sensitiveNameSet 加密参数
     * @param isEncrypt 加解密标识
     */
    private void sensitiveParam(Invocation invocation,Set<String> sensitiveNameSet, boolean isEncrypt){
        if (invocation.getArgs()[1] instanceof MapperMethod.ParamMap) {
            @SuppressWarnings("unchecked")
            Map<String, Object> stringObjectMap = (Map<String, Object>) invocation.getArgs()[1];
            sensitiveParamMap(stringObjectMap, sensitiveNameSet, isEncrypt);
            return;
        }

        invocation.getArgs()[1] = sensitiveParamOther(invocation.getArgs()[1], sensitiveNameSet, isEncrypt);
    }


    /**
     * 处理 类型为 paramMap
     * @param parameterObject
     * @param sensitiveNameSet
     */
    private void sensitiveParamMap(Map<String, Object> parameterObject, Set<String> sensitiveNameSet, boolean isEncrypt) {
        for (String key : sensitiveNameSet) {
            // 获取需要加密的 value
            Object value = parameterObject.get(key);

            // 值为字符串
            if (value instanceof String) {
                parameterObject.put(key, sensitiveString((String) value, isEncrypt));
            }

            // 值为集合, 没有处理集合为不可变类型 如: Collections.singletonList()
            else if(value instanceof List) {
                @SuppressWarnings("unchecked")
                List<Object> list = (List<Object>) value;
                sensitiveList(list, isEncrypt);
            }

            // 其他类型处理
            else {
                sensitiveObject(value, isEncrypt);
            }
        }
    }



    /**
     * 处理其他类型,直接只有一个参数类型,没有进行 paramMap包装,所以需要单独解析
     * @param parameterObject
     * @param sensitiveNameSet
     * @return
     */
    private Object sensitiveParamOther(Object parameterObject, Set<String> sensitiveNameSet, boolean isEncrypt) {
        // 只有一个形参的情况下 没有加密注解 直接跳过
        if (sensitiveNameSet.isEmpty())
            return parameterObject;

        if (parameterObject instanceof String) {
            return sensitiveString((String) parameterObject, isEncrypt);
        }

        // 值为集合, 没有处理集合为不可变类型 如: Collections.singletonList()
        else if (parameterObject instanceof List) {
            @SuppressWarnings("unchecked")
            List<Object> list = (List<Object>) parameterObject;
            sensitiveList(list, isEncrypt);
        }

        // 处理其他类型,
        else {
            sensitiveObject(parameterObject, isEncrypt);
        }

        return parameterObject;
    }



    /**
     * 加密 list
     * @param list
     */
    private void sensitiveList(List<Object> list, boolean isEncrypt) {
        for (int i = 0; i < list.size(); i++) {
            Object val = list.get(i);
            if (val instanceof String) {
                list.set(i, sensitiveString((String) val, isEncrypt));
            } else {
                sensitiveObject(val, isEncrypt);
            }
        }
    }



    /**
     * 加密普通对象
     * @param parameterObject
     */
    private void sensitiveObject(Object parameterObject, boolean isEncrypt) {
        // 过滤调一些不需要加密的类型,
        if (parameterObject instanceof Map || parameterObject instanceof Collection
                || parameterObject instanceof Number || parameterObject instanceof Boolean
                || parameterObject instanceof String || parameterObject instanceof Date)
            return;

        // 如果是普通对象,检查是否有@SensitiveField注解的字段
        // 只处理普通对象
        Class<?> clazz = parameterObject.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (Objects.isNull(field.getAnnotation(SensitiveParam.class))) // 验证数据是否需要加密
                continue;

            try {// map 和 其他类型 不进行处理
                field.setAccessible(true);
                Object value = field.get(parameterObject);
                if (value  instanceof String) {
                    field.set(parameterObject, sensitiveString((String) value, isEncrypt));
                } else if (value  instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<Object> list = (List<Object>) value;
                    sensitiveList(list, isEncrypt);
                } else {
                    sensitiveObject(value, isEncrypt);
                }
            } catch (Exception e){
                log.error("数据加密异常!", e);
            } finally {
                field.setAccessible(false);
            }

        }
    }



    /**
     *
     * @param value 加解密字符串
     * @param isEncrypt true(加密) false(解密)
     * @return
     */
    private String sensitiveString(String value, boolean isEncrypt) {
        return isEncrypt ? EncryptUtil.encrypt(value, AES_KEY)
                : EncryptUtil.decrypt(value, AES_KEY);
    }


    /**
     * 处理动态参数
     * @param mappedStatement
     * @param args
     */
    private void setBoundSql(MappedStatement mappedStatement, Object[] args) {
        if(args.length == 6 && Objects.nonNull(args[5])){
            args[5]  = mappedStatement .getBoundSql(args[1]);
        }
    }

        4. 实现返回参数解密:

                方法标识 SensitiveMethodResult注解
/**
 *  方法标识  返参解密
 *  当方法添加此注解 返参参数为 string 或者 list<String> 是会对内部参数进行解密
 *  当返参参数为普通对象时,只有内部属性添加 SensitiveResult 注解的 才会进行解密
 */
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMethodResult {
}
                返回参数标识SensitiveResult注解
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveResult {
}
                 拦截器实现:
@Component
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class RetExecuteInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(RetExecuteInterceptor.class);

    private final String AES_KEY = "qwertyuiopasdfgh";

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

        // 执行方法, 获取当前方法
        Object proceed = invocation.proceed();
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Method interfaceMethod = InterceptorUtil.getInterfaceMethod(mappedStatement.getId());

        if(Objects.isNull(proceed) || Objects.isNull(interfaceMethod) || isNotSensitiveMethodResult(interfaceMethod))
            return proceed;


        // 处理返回类型, 返回类型只有 集合 和 数字, 数字不处理 只处理集合
        if(proceed instanceof ArrayList) {
            @SuppressWarnings("unchecked")
            ArrayList<Object> resultList = (ArrayList<Object>) proceed;

            for (int i = 0; i < resultList.size(); i++) {
                Object obj = resultList.get(i);

                // 为空数据直接跳过
                if(Objects.isNull(obj))
                    continue;

                // 字符串直接解密
                if(obj instanceof String) {
                    resultList.set(i, EncryptUtil.decrypt((String) obj, AES_KEY));
                    continue;
                }

                decryptObject(obj);
            }
        }

        return proceed;
    }



    /**
     * 验证当前方法是否为加密参数方法
     *
     * @param interfaceMethod
     * @return
     */
    private boolean isNotSensitiveMethodResult(Method interfaceMethod) {
        SensitiveMethodResult annotation = interfaceMethod.getAnnotation(SensitiveMethodResult.class);
        return Objects.isNull(annotation);
    }


    /**
     * 处理对象类型
     * @param obj 解密的对象
     */
    private void decryptObject(Object obj){
        // 过滤调一些不需要解密的类型,防止类型错误导致死循环
        if (obj instanceof Map || obj instanceof Collection || obj instanceof Number ||
                obj instanceof Boolean || obj instanceof String || obj instanceof Date)
            return;

        // 如果是普通对象,检查是否有 @SensitiveResult 注解的字段
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {

            // 验证数据是否需要加密
            if (Objects.isNull(field.getAnnotation(SensitiveResult.class)))
                continue;

            try {// 只处理字符串类型
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value instanceof String) {
                    field.set(obj, EncryptUtil.decrypt((String) value, AES_KEY));
                }
            } catch (Exception e){
                log.error("数据加密异常!", e);
            } finally {
                field.setAccessible(false);
            }
        }
    }
                InterceptorUtil实现:
public class InterceptorUtil {

    private static final Logger log = LoggerFactory.getLogger(InterceptorUtil.class);


    /**
     * 获取 接口方法
     *
     * @param mappedStatement mybatis 拦截器 Interceptor 参数
     * @return 接口方法
     */
    public static Method getInterfaceMethod(String statementId) {
        // 获取MappedStatement的 id,格式为namespace.id
        try {
            // 获取 namespace 和 id
            int lastDot = statementId.lastIndexOf(".");
            String namespace = statementId.substring(0, lastDot);
            String methodId = statementId.substring(lastDot + 1);
            Class<?> mapperInterface = Class.forName(namespace);// 获取接口类型
            for (Method method : mapperInterface.getDeclaredMethods()) {// 获取接口方法
                if (method.getName().equals(methodId))
                    return method;
            }
        } catch (Exception e) {
            log.error("方法获取失败,获取属性: {}", statementId);
        }

        return null;
    }

         5. 案例:

    @SensitiveMethodParam
    int insertEdcMonitorLog(@SensitiveParam EdcMonitorLog edcMonitorLog);

    @SensitiveMethodParam
    int insertEdcMonitorLog2(@SensitiveParam List<String> edcMonitorLog);

    @SensitiveMethodParam
    int insertEdcMonitorLog3(@SensitiveParam List<EdcMonitorLog> edcMonitorLog);

    @SensitiveMethodParam
    @SensitiveMethodResult
    List<EdcMonitorLog> selectEdcMonitorLog(@SensitiveParam String msgId);

    @SensitiveMethodParam
    @SensitiveMethodResult
    List<EdcMonitorLog> selectEdcMonitorLog2(@SensitiveParam List<String> msgId);

    @SensitiveMethodParam
    @SensitiveMethodResult
    List<EdcMonitorLog> selectEdcMonitorLog3(@SensitiveParam List<EdcMonitorLog> list);

    @SensitiveMethodParam
    @SensitiveMethodResult
    List<String> selectMsgId(@SensitiveParam @Param("list1") List<String> list1, @Param("list2") List<String> list2);
@Data
public class EdcMonitorLog {

    @SensitiveParam
    @SensitiveResult
    private String msgId;

}

         优点:

                    1. 对代码没有侵入,对sql也没有修改

                    2. 可以通过数据库对参数进行直接解密

          缺点:

                    1. 如果参数比较复杂的情况下,多级嵌套,可能影响效率

                    2. 获取方法没有进行优化(getInterfaceMethod) 可以缓存解析结果,本人偷个懒,期待大佬帮忙优化一下

                    3. 普通对象需要加密的字段也可以缓存(期待大佬优化)

                    4. 当前只加密了字符串类型,其他类型没有处理(没有需求)

                    5. 程序加密的AES算法要求的密钥长度必须是128, 192或256位,但是使用mysql是没有影响的

六:mybatis插件原理

Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:

Executor:执行CURD操作;
StatementHandler:处理sql语句预编译,设置参数等相关工作;
ParameterHandler:设置预编译参数用的;
ResultSetHandler:处理结果集。

        方案一:拦截预编译sql

        方案二:拦截执行器 query、update方法

MyBatis拦截器和自定义注解MyBatis框架中的两个重要特性。下面我会分别解释它们的作用和用法。 MyBatis拦截器是一种机制,可以在执行SQL语句的过程中对其进行拦截和修改。它提供了一种方便的方式来扩展和自定义MyBatis的功能。拦截器可以在SQL语句执行前后、参数设置前后、结果集处理前后等关键点进行拦截,并对其进行修改或增强。 要实现一个MyBatis拦截器,你需要实现`Interceptor`接口,并重写其中的方法。其中最重要的方法是`intercept`,它接收一个`Invocation`对象作为参数,通过该对象你可以获取到当前执行的SQL语句、参数等信息,并可以对其进行修改。另外还有`plugin`方法和`setProperties`方法用于对拦截器进行初始化。 自定义注解是一种用于标记和配置特定功能的注解。在MyBatis中,你可以使用自定义注解来配置一些特殊的功能,比如动态SQL的条件判断、结果集映射等。通过自定义注解,你可以将一些常用的功能封装成注解,并在需要时直接使用。 要使用自定义注解,你需要先定义一个注解,并在相应的地方使用该注解。然后通过MyBatis的配置文件或者Java代码进行配置,告诉MyBatis如何处理这些注解。在MyBatis的执行过程中,它会根据注解的配置来动态生成相应的SQL语句或者进行特定的处理。 总结一下,MyBatis拦截器和自定义注解MyBatis框架中的两个重要特性。拦截器可以对SQL语句进行拦截和修改,自定义注解可以用于配置一些特殊功能。它们都提供了一种扩展和自定义MyBatis功能的方式。如果你有具体的问题或者需要更详细的示例代码,欢迎继续提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值