前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot集成Mybatis

SpringBoot集成Mybatis

原创
作者头像
后台技术汇
发布2024-11-16 11:04:11
230
发布2024-11-16 11:04:11
举报
文章被收录于专栏:后台技术汇

背景

Mybatis是一个半自动化的ORM(对象关系映射)框架,它通过XML或注解的方式将Java对象和SQL语句进行映射,从而实现对数据库的增删改查操作。

Mybatis ORM简介

  • 基本概念:Mybatis简化了数据访问层的开发,通过映射配置文件或注解将Java对象与数据库表进行映射,实现了对象关系映射。
  • 优点:Mybatis提供了灵活的SQL编写方式,支持动态SQL,易于与Spring等框架集成,同时减少了代码量,提高了开发效率。
  • 缺点:相比于全自动化的ORM框架如Hibernate,Mybatis需要编写更多的SQL语句,对开发人员的SQL编写能力有一定要求

Mybatis ORM的使用过程

  • 创建SqlSessionFactory:可以通过配置文件或直接编码来创建SqlSessionFactory。
  • 使用SqlSession进行数据库操作:通过SqlSession执行SQL语句,获取结果,并映射到Java对象。

引入依赖

代码语言:javascript
复制
<!-- 整合mybatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<!-- 数据库驱动(例如 MySQL) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

数据库配置

数据库表初始化

代码语言:javascript
复制
CREATE TABLE `user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(255) NOT NULL DEFAULT '' COMMENT '账号',
    `age` int(2) NOT NULL DEFAULT 0 COMMENT '年龄',
    `email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱',
    `tenant_id` bigint(20) NOT NULL DEFAULT 1 COMMENT '租户id,1表示默认租户',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

xml文件映射sql:UserMapper.xml

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bryant.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="com.bryant.model.UserDetail">
         <id column="id" property="id" jdbcType="INTEGER"/>
         <result column="name" property="name" jdbcType="VARCHAR"></result>
         <result column="age" property="age" jdbcType="INTEGER"></result>
         <result column="email" property="email" jdbcType="VARCHAR"></result>
         <result column="tenant_id" property="tenantId" jdbcType="BIGINT"></result>
    </resultMap>

    <sql id="base_columns">
        `id`, `name`, age, email, tenant_id
    </sql>

    <insert id="insert" parameterType="map" keyColumn="id">
        insert
        into user
            (id, name, age, email, tenant_id)
        values
           (#{record.id,jdbcType=INTEGER},
            #{record.name,jdbcType=VARCHAR},
            #{record.age,jdbcType=INTEGER},
            #{record.email,jdbcType=VARCHAR},
            #{record.tenantId,jdbcType=BIGINT}
            )
    </insert>

    <update id="updateById" parameterType="map">
        update user
        set `age` = #{record.age},
            `name` = #{record.name},
            `email` = #{record.email}
        where id = #{record.id}
    </update>

    <delete id="deleteById">
        delete from user
        where id = #{id}
    </delete>

    <select id="getById" resultMap="BaseResultMap">
        select
        <include refid="base_columns"/>
        from user
        where id = #{id}
            and tenant_id = #{tenantId}
    </select>

</mapper>

应用配置

启动类

代码语言:javascript
复制
//...其他注解
// mapper扫描的包路径
@MapperScan("com.bryant.mapper")
@SpringBootApplication
public class UserServer {

   public static void main(String[] args) {
       SpringApplication.run(UserServer.class, args);
   }

}

注意标黄的代码段@MapperScan,是对包路径的扫描。

Mybatis配置

代码语言:javascript
复制
/**
 * Full(proxyBeanMethods = true) :proxyBeanMethods参数设置为true时即为:Full 全模式。该模式下注入容器中的同一个组件无论被取出多少次都是同一个bean实例,即单实例对象,在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件
 * Lite(proxyBeanMethods = false) :proxyBeanMethods参数设置为false时即为:Lite 轻量级模式。该模式下注入容器中的同一个组件无论被取出多少次都是不同的bean实例,即多实例对象,在该模式下SpringBoot每次启动会跳过检查容器中是否存在该组件
 */
@Configuration(proxyBeanMethods = true)
@ConditionalOnProperty(prefix = "users.mybatis.custom", name = "interceptor", havingValue = "true")
@Slf4j
public class MybatisConfig implements InitializingBean {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactorys;

    @Bean
    @Order(1)
    public TenantIdInjectInterceptor mybatisInterceptor() {
        log.info("TenantIdInjectInterceptor interceptor init...");
        return new TenantIdInjectInterceptor();
    }

    /**
     * @Order(0) 是为了保证 TenantIdInjectInterceptor拦截器先于sqlMonitorInterceptor拦截器执行
     * @return
     */
    @Bean
    @Order(0)
    public SqlMonitorInterceptor sqlMonitorInterceptor() {
        log.info("SqlMonitorInterceptor interceptor init...");
        return new SqlMonitorInterceptor();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
    }

}

Mybatis插件配置

SQL租户ID检测插件-TenantIdInjectInterceptor

可以参考上2篇文章:Mybatis插件-租户ID的注入&拦截应用Mybatis链路分析:JDK动态代理和责任链模式的应用

代码语言:javascript
复制
/**
 * 拦截器做了2个事情
 * - 1.打印原sql
 * - 2.注入自定义参数
 *
 * Mybatis 插件的执行顺序有两种:
 * 1、不同拦截对象执行顺序,如下:
 *     Executor` -> `StatementHandler` -> `ParameterHandler` -> `ResultSetHandler`
 * 2、拦截相同对象执行顺序,取决于 mybatis-config.xml 中 <plugin> 配置顺序,越靠后,优先级越高。
 * 3、拦截相同对象执行顺序,如果是使用的配置类加载,则取决于配置类的加载顺序,加载顺序,越靠后,优先级越高;
 */
@Slf4j
@Intercepts(value = {
        @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}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class TenantIdInjectInterceptor implements Interceptor {

    //用于匹配字符串中的tenant_id关键字,后面可以跟任意数量的空格,然后是等号=,再后面可以跟任意数量的空格,最后是一个问号?。
    // 这个正则表达式使用了Pattern.CASE_INSENSITIVE标志,表示匹配时不区分大小写。
    // 匹配问号,因为问号在正则表达式中有特殊含义(表示前面的字符出现0次或1次),所以需要用两个反斜杠进行转义
    private static final Pattern p = Pattern.compile("tenant_id(\\s+)?=(\\s+)?\\?", Pattern.CASE_INSENSITIVE);
    private static final String SQL_IGNORED_CHARACTOR = "[\\t\\n\\r]";
    /**
     * 核心逻辑在intercept方法,内部实现 sql 获取,参数解析,耗时统计
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("TenantIdInjectInterceptor interceptor start...");

        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        String namespace = mappedStatement.getId();
        String className = namespace.substring(0, namespace.lastIndexOf("."));
        String methodName = mappedStatement.getId()
                .substring(mappedStatement.getId().lastIndexOf(".") + 1);
        Class<?> c = Class.forName(className);

        //如果Class有注解指明要忽略本次拦截,则放弃拦截
        if (c.isAnnotationPresent(TenantIdInjectConfig.class) && c.getAnnotation(TenantIdInjectConfig.class).ignore()) {
            return invocation.proceed();
        }

        Method[] ms = c.getMethods();
        Method method = Arrays.stream(ms).filter(m -> m.getName().equals(methodName)).findAny().get();
        //如果method 有注解指明要忽略本次拦截,则放弃拦截
        if (method.isAnnotationPresent(TenantIdInjectConfig.class) && method.getAnnotation(TenantIdInjectConfig.class)
                .ignore()) {
            return invocation.proceed();
        }

        //判断SQL中是否存在tenant_id 字段,如果存在,认为已经考虑多租户的情况,否则将SQL拦截下来
        BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(args[1]);
        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll(SQL_IGNORED_CHARACTOR, " ");
        boolean sqlWithTenantIdParam;
        if (mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)) {
            //insert语句只判断是否有tenant_id
            sqlWithTenantIdParam = sql.contains("tenant_id");
        } else {
            //其他语句判断是否有tenant_id=?
            sqlWithTenantIdParam = p.matcher(sql).find();
        }

        if (!sqlWithTenantIdParam) {
            log.error("缺少对多租户的支持,tenant_id 字段缺失,sql:{}", sql);
            throw new RuntimeException("缺少对多租户的支持,tenant_id字段缺失");
        }

        //这里使用默认的租户id=1
        String defaultTenantId = "1";
        Map map;
        if (args[1] == null) {
            //没有参数列表
            map = new MapperMethod.ParamMap<>();
        } else if (!(args[1] instanceof MapperMethod.ParamMap)) {
            //单参数
            Map tempMap = new MapperMethod.ParamMap<>();
            Parameter[] parameters = method.getParameters();
            Parameter param = parameters[0];
            //第一个参数获取@Param注解,然后获取值
            if (param.isAnnotationPresent(Param.class)) {
                String paramName = param.getAnnotation(Param.class).value();
                tempMap.put(paramName, args[1]);
            } else if (checkTypeType(param)) {
                //如果是基础类型抛出异常
                tempMap.put(param.getName(), args[1]);
            } else {
                //如果没有指定@Param,将单参数的属性单独拎出来处理
                Object arg = args[1];
                Field[] fields = arg.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (!Modifier.isStatic(field.getModifiers())) {
                        if (!field.isAccessible()) {
                            field.setAccessible(true);
                        }
                        tempMap.put(field.getName(), field.get(arg));
                    }
                }
            }
            args[1] = tempMap;
        }

        //如果是多参数列表时直接转map即可
        map = (Map) args[1];
        if (!StringUtils.isBlank(defaultTenantId)) {
            map.put("tenantId", Long.parseLong(defaultTenantId));
        }

        return invocation.proceed();
    }

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

    private boolean checkTypeType(Object object) {
        if (object.getClass().isPrimitive()
                || object instanceof String
                || object instanceof Boolean
                || object instanceof Double
                || object instanceof Float
                || object instanceof Long
                || object instanceof Integer
                || object instanceof Byte
                || object instanceof Short) {
            return true;
        } else {
            return false;
        }
    }
}

SQL监控插件-SqlMonitorInterceptor

代码语言:javascript
复制
@Slf4j
@Intercepts(value = {
        @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}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class SqlMonitorInterceptor implements Interceptor {

    private static final String SQL_IGNORED_CHARACTOR = "[\\t\\n\\r]";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("SqlMonitorInterceptor interceptor start...");

        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(args[1]);
        // 1.打印原sql
        log.info("SqlMonitorInterceptor original sql: {}", boundSql.getSql().toLowerCase(Locale.CHINA).replace(SQL_IGNORED_CHARACTOR, " "));

        // BoundSql就是封装myBatis最终产生的sql类

        String sqlId = mappedStatement.getId();
        // 获取节点的配置
        Configuration configuration = mappedStatement.getConfiguration();
        // 获取到最终的sql语句
        log.info("SqlMonitorInterceptor sql after parse : {}", getSql(configuration, boundSql, sqlId));
        return invocation.proceed();
    }

    // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号;对参数是null和不是null的情况作了处理
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
                    DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }


    /**
     * 进行 ?符号的替换
     * @param configuration
     * @param boundSql
     * @return
     */
    private static String parseSql(Configuration configuration, BoundSql boundSql) {
        // 获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // sql语句中多个空格都用一个空格代替
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
            // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?",
                        Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        // 该分支是动态sql
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 打印出缺失,提醒该参数缺失并防止错位
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
     * @param configuration
     * @param boundSql
     * @param sqlId
     * @return
     */
    private static String getSql(Configuration configuration, BoundSql boundSql, String sqlId)
    {
        String sql = parseSql(configuration, boundSql);
        StringBuilder str = new StringBuilder(100);
        str.append(sqlId);
        str.append(":");
        str.append(sql);
        return str.toString();
    }

}

自定义租户配置注解

代码语言:javascript
复制
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantIdInjectConfig {

    boolean ignore() default false;

}

插件配置到SqlSession

代码语言:javascript
复制
/**
 * Full(proxyBeanMethods = true) :proxyBeanMethods参数设置为true时即为:Full 全模式。该模式下注入容器中的同一个组件无论被取出多少次都是同一个bean实例,即单实例对象,在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件
 * Lite(proxyBeanMethods = false) :proxyBeanMethods参数设置为false时即为:Lite 轻量级模式。该模式下注入容器中的同一个组件无论被取出多少次都是不同的bean实例,即多实例对象,在该模式下SpringBoot每次启动会跳过检查容器中是否存在该组件
 */
@Configuration(proxyBeanMethods = true)
@ConditionalOnProperty(prefix = "users.mybatis.custom", name = "interceptor", havingValue = "true")
@Slf4j
public class MybatisConfig implements InitializingBean {

    @Bean
    @Order(1)
    public TenantIdInjectInterceptor mybatisInterceptor() {
        log.info("TenantIdInjectInterceptor interceptor init...");
        return new TenantIdInjectInterceptor();
    }

    /**
     * @Order(0) 是为了保证 TenantIdInjectInterceptor拦截器先于sqlMonitorInterceptor拦截器执行
     * @return
     */
    @Bean
    @Order(0)
    public SqlMonitorInterceptor sqlMonitorInterceptor() {
        log.info("SqlMonitorInterceptor interceptor init...");
        return new SqlMonitorInterceptor();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
    }

}

分析:

  • 这里将插件定义bean,交给容器管理即可,Mybatis会自动检测并加载到SqlSession

配置中心

数据库配置 - users-dev.properties

代码语言:javascript
复制
## mybatis依赖
spring.datasource.url = jdbc:mysql://localhost:3306/mac_m1?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=12345678
#数据库名称
spring.datasource.name=mac_m1
#默认情况下mybatis是不开启SQL日志输出,需要手动配置,com.riant.dao为mapper文件夹路径
logging.level.com.riant.mybatis.dao=debug
#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml

服务接口和应用配置

配置中心读取配置-bootstrap.properties

代码语言:javascript
复制
# 使用配置中心的master分支的{service-name}-dev.properties配置内容
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.profile=dev
spring.cloud.config.label=master

服务Mybatis配置-application.properties

代码语言:javascript
复制
server.port=8891

## mybatis依赖
#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml
#默认情况下mybatis是不开启SQL日志输出,需要手动配置,com.riant.dao为mapper文件夹路径
logging.level.com.riant.mybatis.dao=debug

## 自定义sql拦截器
users.mybatis.custom.interceptor=true

controller

代码语言:javascript
复制
@RestController
public class MybatisController {

    @Autowired
    private UserService userService;

    @PostMapping("/user_insert")
    public UserDetail user_insert() {
        UserDetail detail = UserDetail.builder()
                .age(new Random().nextInt(100))
                .email(new Random().nextInt(100000000) + "@qq.com")
                .name("bryant" + new Random().nextInt(1111))
                .tenantId(new Random().nextLong())
                .build();
        return userService.insert(detail);
    }

    @GetMapping("/user_select")
    public UserDetail user_select(@RequestParam("id") Long id) {
        return userService.getById(id);
    }

}

service

代码语言:javascript
复制
public interface UserService {

    UserDetail insert(UserDetail detail);

    UserDetail getById(Long id);

    void update(UserDetail detail);

    void delete(Long id);

}


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetail insert(UserDetail detail) {
        if (Objects.isNull(detail)) {
            throw new RuntimeException("detail is null");
        }
        userMapper.insert(detail);
        return detail;
    }

    @Override
    public UserDetail getById(Long id) {
        return userMapper.getById(id);
    }

    @Override
    public void update(UserDetail detail) {
        userMapper.updateById(detail);
    }

    @Override
    public void delete(Long id) {
        userMapper.deleteById(id);
    }
}

mapper

代码语言:javascript
复制
@Repository
public interface UserMapper {

    void insert(@Param("record") UserDetail user);

    @TenantIdInjectConfig(ignore = true)
    void updateById(@Param("record") UserDetail user);

    @TenantIdInjectConfig(ignore = true)
    void deleteById(@Param("id") Long id);

    UserDetail getById(@Param("id") Long id);

}

model

代码语言:javascript
复制
@Data
@Builder
public class UserDetail implements Serializable {

    private static final long serialVersionUID = 2235541748764244156L;

    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long tenantId;

}

服务测试

注册服务

http://localhost:8881/,可以看到注册中心和user服务已经注册上去了。

添加描述

增删改查

添加描述

添加描述

拦截器日志

代码语言:javascript
复制
2024-08-26 22:51:52.756 DEBUG [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] o.s.web.servlet.DispatcherServlet        : GET "/user_select?id=1", parameters={masked}
2024-08-26 22:51:52.757  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.c.mysql.TenantIdInjectInterceptor    : TenantIdInjectInterceptor interceptor start...
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor interceptor start...
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor original sql: select
         
        `id`, `name`, age, email, tenant_id
     
        from user
        where id = ?
            and tenant_id = ?
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor sql after parse : com.bryant.mapper.UserMapper.getById:select `id`, `name`, age, email, tenant_id from user where id = 1 and tenant_id = 1
2024-08-26 22:51:52.761 DEBUG [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

总结

Mybatis的拦截器,原理上使用了非常多的设计模式,可以参考文章Mybatis链路分析:JDK动态代理和责任链模式的应用Mybatis插件-租户ID的注入&拦截应用

  • 动态代理技术,Mybatis对执行器Executor使用了动态代理增强技术,从而让拦截器得以实现拦截功能;
  • 责任链模式,利用拦截器chain,Executor的各个handler进行非侵入式拦截处理。

SpringBoot集成Mybatis,跟传统的开发有比较大的异同,挖个坑,下文我们继续剖析一下源码流程。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
    • Mybatis ORM简介
      • Mybatis ORM的使用过程
      • 引入依赖
      • 数据库配置
        • 数据库表初始化
        • 应用配置
          • 启动类
            • Mybatis配置
              • Mybatis插件配置
                • SQL租户ID检测插件-TenantIdInjectInterceptor
                • SQL监控插件-SqlMonitorInterceptor
                • 自定义租户配置注解
                • 插件配置到SqlSession
              • 配置中心
                • 数据库配置 - users-dev.properties
              • 服务接口和应用配置
                • 配置中心读取配置-bootstrap.properties
                • 服务Mybatis配置-application.properties
                • controller
                • service
                • mapper
                • model
            • 服务测试
              • 注册服务
                • 增删改查
                  • 拦截器日志
                  • 总结
                  相关产品与服务
                  数据库
                  云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档