当前位置: 首页 > article >正文

SpringBoot集成Mybatis

9561efe1c6f52fe246f36b171e59724c.jpeg


背景

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对象。

d6a743c38e696db18a1049bf1cc17a54.png

引入依赖

<!-- 整合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>

数据库配置

数据库表初始化

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

<?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>

应用配置

启动类

//...其他注解
// mapper扫描的包路径
@MapperScan("com.bryant.mapper")
@SpringBootApplication
public class UserServer {


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


}

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

Mybatis配置

/**
 * 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动态代理和责任链模式的应用

/**
 * 拦截器做了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

@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();
    }


}

自定义租户配置注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantIdInjectConfig {


    boolean ignore() default false;


}

插件配置到SqlSession

/**
 * 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

## 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

# 使用配置中心的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

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

@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

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

@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

@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服务已经注册上去了。

79fd419142fc5b917a26e167ed9ee299.png

增删改查

7a414724466d4fea0a54eb57b4e4c8e4.png

1d4b231bdf9c52e9f717b9d00d40ec57.png

拦截器日志

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,跟传统的开发有比较大的异同,挖个坑,下文我们继续剖析一下源码流程。


http://www.kler.cn/a/379866.html

相关文章:

  • csp2024T3
  • 基于SpringBoot+微信小程序+协同过滤算法+二维码订单位置跟踪的农产品销售平台-新
  • 运维工具之docker入门
  • Android 使用ninja加速编译的方法
  • java、excel表格合并、指定单元格查找、合并文件夹
  • 人脑与机器连接:神经科技的伦理边界探讨
  • Windows 系统安装 Hadoop 详细教程
  • 交换机如何实现2.5G网络传输速率和网络变压器有关吗
  • 深度学习-42-基于PyTorch对LeNet5逐层分析计算过程
  • RSI是指在5G通信技术中用于标识小区的特定参数
  • 【ACM出版,EI稳定检索】2024年人工智能、数字媒体技术与交互设计国际学术会议(ICADI 2024,11月29-12月1日)
  • 深入解析 Memcached原理、架构与最佳实践
  • 02多线程基础知识
  • 100种算法【Python版】第41篇——Chan‘s 算法
  • 介质访问控制方法
  • 一次家庭组网,耗时40分钟
  • Minio中出现Non-XML response from server异常
  • 数据迁移: 安全高效转移数据, 满足企业业务需求和技术改进
  • 使用labelme中的AI模型提升数据标注速度
  • CSS基础概念:什么是 CSS ? CSS 的组成
  • 11.Node.js API接口
  • 《双指针篇》---盛最多水的容器_Java(中等但简单)
  • 在VS中安装chatGPT
  • 什么是声明式编程什么是函数式编程,打比方说明
  • windows C#技术-概述
  • 算法:只出现一次的数字II