MyBatis使用:拦截器,SpringBoot整合MyBatis

news2024/11/7 17:04:47

1、目标

本文的主要目标是学习使用MyBatis拦截器,并给出拦截器的实例

2、拦截器的使用

2.1 @Intercepts注解和@Signature注解

@Intercepts注解,指定拦截哪个拦截器的哪个方法,还要指定参数,因为可能发生方法重载

按照顺序可以拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler这4个接口的方法

@Signature注解,指定type是这4个接口中的某个接口,指定method是接口中的一个方法,指定args是方法的入参

2.2 四个接口

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

Executor的拦截器在SQL语句执行之前和之后执行,可以在这里添加逻辑来处理SQL执行过程中的需求,如日志记录或性能监控

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

StatementHandler的拦截器在生成SQL语句和设置参数阶段执行,可以在这里修改SQL语句或参数,也可以打印日志

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}

ParameterHandler的拦截器在StatementHandler设置参数之后执行

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

ResultSetHandler的拦截器在SQL执行之后、将结果集映射到Java对象之前执行,可以在这里对查询结果进行修改或处理

2.3 Interceptor接口

类需要实现Interceptor接口,重写intercept方法

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

Interceptor接口的plugin方法和setProperties方法都是default默认方法,可以不重写

Interceptor接口的intercept方法的入参是Invocation对象

public class Invocation {
  private final Object target;
  private final Method method;
  private final Object[] args;
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
}

Invocation对象的target属性是被代理对象,即4个接口中的一个,method是接口的方法,args是方法的入参

Invocation对象的proceed方法是执行被代理对象的方法,因此拦截器可以在执行被代理对象的方法之前或者之后进行操作,它还有返回值就是被代理对象的方法的返回值

2.4 @Component

实现Interceptor接口的类需要加上@Component注解,将它注入到IOC容器中

3、拦截器的实例

3.1 设计表

create table `news` (
    `id` varchar(100) primary key not null,
    `title` varchar(100) not null,
    `create_user_id` int,
    `create_date_time` date
)

3.2 整体类结构

在这里插入图片描述

3.3 pom.xml文件

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.27</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>

    <!--MD5依赖-->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.16.0</version>
    </dependency>
</dependencies>

3.4 application.yml文件

server:
  port: 9021

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:port/数据库名字?useUnicode=true
    username: xxx
    password: xxx

mybatis:
  # 扫描resource类路径下的所有xml文件
  mapper-locations: classpath:/*.xml
  # 输出sql
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  # 扫描包下的所有类,首字母小写作为别名
  type-aliases-package: com.lwc

如果没有配置数据源spring.datasource的话,启动服务的时候会报错

在这里插入图片描述

如果没有配置mybatis.mapper-locations是xml文件的位置,就会抛出异常:org.apache.ibatis.binding.BindingException

在这里插入图片描述

mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 是输出sql语句

mybatis.type-aliases-package: com.lwc 是com.lwc包下的所有类都取别名是类名首字母小写,在写resultType的时候不用写全类名了,直接写类名首字母小写

3.5 创建主启动类MybatisIntercetorApplication

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

3.6 controller接口

@RestController
@RequiredArgsConstructor
@Log4j2
@RequestMapping("/news")
public class NewsController {

    private final NewsService newsService;

    @PostMapping("/save")
    public String saveNews(@RequestBody Map<String, Object> map) {
        newsService.saveNews(map);
        return "saveNews success";
    }

}

3.7 service接口

@Service
@RequiredArgsConstructor
@Log4j2
public class NewsService {

    private final NewsMapper newsMapper;

    public void saveNews(Map<String, Object> map) {
        List<String> fieldNameList = new ArrayList<>();
        List<Object> fieldValueList = new ArrayList<>();
        map.forEach((k, v) -> {
            fieldNameList.add(k);
            fieldValueList.add(v);
        });
        newsMapper.insertNews(fieldNameList, fieldValueList);
    }

}

3.8 mapper接口

@Mapper
public interface NewsMapper {
    public int insertNews(@Param("fieldNameList") List<String> fieldNameList,
                          @Param("fieldValueList") List<Object> fieldValueList);
}

3.9 mapper.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.lwc.mapper.NewsMapper">
    <insert id="insertNews">
        insert into news
        <foreach collection="fieldNameList" item="fieldName" open="(" separator="," close=")">
            ${fieldName}
        </foreach>
        values
        <foreach collection="fieldValueList" item="fieldValue" open="(" separator="," close=")">
            #{fieldValue}
        </foreach>
    </insert>
</mapper>

其中,字段名字必须用${},它可以直接替换成字符串,参数值一般用#{},因为它可以防止sql注入,#{}会替换成?,然后?会替换成参数值,如果参数值是字符串会在字符串加上单引号

3.10 ModifySqlInterceptor拦截器

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 目标:在执行StatementHandler对象的prepare方法之前修改sql,因此修改sql之后再调用invocation.proceed()方法
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.info("modify before, sql: {}", sql);
        // parameterObject参数必须是Map,并且map包含fieldNameList和fieldNameList,才会修改sql
        Object parameterObject = boundSql.getParameterObject();
        if(!(parameterObject instanceof Map)){
            return invocation.proceed();
        }
        Map<String, Object> map = (Map<String, Object>) parameterObject;
        if(!map.containsKey("fieldNameList") || !map.containsKey("fieldValueList")) {
            return invocation.proceed();
        }
        // 修改sql,添加多个字段
        List<String> fieldNameList = (List<String>) map.get("fieldNameList");
        fieldNameList.add("id");
        fieldNameList.add("create_user_id");
        fieldNameList.add("create_date_time");
        String fieldName = fieldNameList.stream().collect(Collectors.joining(", "));
        String fieldValue = fieldNameList.stream().map(s -> "?").collect(Collectors.joining(", "));
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(sql.substring(0, sql.indexOf("(") + 1));
        stringBuilder.append(" ");
        stringBuilder.append(fieldName);
        stringBuilder.append(" ");
        stringBuilder.append(sql.substring(sql.indexOf(")"), sql.lastIndexOf("(") + 1));
        stringBuilder.append(" ");
        stringBuilder.append(fieldValue);
        stringBuilder.append(" )");
        String newSql = stringBuilder.toString();
        // 采用SystemMetaObject.forObject方法实例化BoundSql对象
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        metaObject.setValue("sql", newSql);
        log.info("modify after, sql: {}", boundSql.getSql());
        return invocation.proceed();
    }

}

ModifySqlInterceptor拦截器用来在执行StatementHandler对象的prepare方法之前修改sql,因此修改sql之后再调用invocation.proceed()方法

修改sql需要先得到原来的sql,还要得到参数名字和参数值,它们都可以通过StatementHandler对象的BoundSql对象获取到sql和parameterObject,将参数值全部用?替换可以防止sql注入,最后使用SystemMetaObject.forObject方法实例化BoundSql对象并调用metaObject.setValue(“sql”, newSql)方法修改sql

其中,SystemMetaObject.forObject方法是MyBatis用来反射对象的工具类,它可以获取对象属性,也可以设置对象属性

3.11 AddParamsInterceptor拦截器

@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})
})
@Component
@Log4j2
public class AddParamsInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 目标:在StatementHandler对象的parameterize方法完成参数赋值之后再添加新的参数值,因此先执行invocation.proceed()方法然后添加参数值
        // StatementHandler对象的parameterize方法返回值是void
        invocation.proceed();
        // 获取StatementHandler对象中的sql得到表名
        RoutingStatementHandler routingStatementHandler = (RoutingStatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(routingStatementHandler);
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");
        String tableName = sql.substring(sql.indexOf("into") + 5, sql.indexOf("(")).trim();
        // 获取StatementHandler对象中的参数名和参数值,得到Map
        Object parameterObject = metaObject.getValue("delegate.boundSql.parameterObject");
        if(!(parameterObject instanceof Map)){
            return invocation.proceed();
        }
        Map<String, Object> map = (Map<String, Object>) parameterObject;
        if(!map.containsKey("fieldNameList") || !map.containsKey("fieldValueList")) {
            return invocation.proceed();
        }
        List<String> fieldNameList = (List<String>) map.get("fieldNameList");
        List<Object> fieldValueList = (List<Object>) map.get("fieldValueList");
        Map<String, Object> res = new HashMap<>();
        for(int i = 0; i < fieldValueList.size(); i++) {
            res.put(fieldNameList.get(i).toLowerCase(), fieldValueList.get(i));
        }
        // 获取StatementHandler对象中的parameterMappings,得到参数下标,因为parameterize方法的参数值赋值是根据parameterMappings的大小来的
        List<ParameterMapping> parameterMappings = (List<ParameterMapping>) metaObject.getValue("delegate.boundSql.parameterMappings");
        int size = parameterMappings.size();
        // 添加参数值,需要按照顺序,根据修改的sql的字段顺序
        Object[] args = invocation.getArgs();
        PreparedStatement preparedStatement = (PreparedStatement) args[0];
        preparedStatement.setString(++size, getId(tableName, res));
        preparedStatement.setInt(++size, UserIdThreadLocal.getUserId());
        preparedStatement.setDate(++size, new Date(System.currentTimeMillis()));
        return null;
    }

    private String getId(String tableName, Map<String, Object> map) {
        if("news".equals(tableName.toLowerCase())) {
            String title = (String) map.get("title");
            String id = MD5Util.getMd5(title);
            return id;
        }
        return null;
    }

}

AddParamsInterceptor拦截器用来在StatementHandler对象的parameterize方法完成参数赋值之后再添加新的参数值,因此先执行invocation.proceed()方法然后添加参数值

添加参数值是通过parameterize方法的入参PreparedStatement对象的setXxx方法实现的,同时可以防止sql注入因为参数值是字符串的话会加单引号,同时添加参数值的顺序要和ModifySqlInterceptor拦截器添加参数名字的顺序一致

setXxx方法的key:

获取StatementHandler对象中的parameterMappings,得到参数下标,因为parameterize方法的参数值赋值是根据parameterMappings的大小来的

setXxx方法的value:

① 参数id的值是根据表名tableName和title这个字段的值使用MD5哈希算法得到id值,id值是一个字符串因此用setString方法

② 参数create_user_id的值是根据ThreadLocal中的userId得到的,这个userId是前端发送请求会携带userId请求头然后在过滤器解析请求头中的userId并存放到ThreadLocal中的,create_user_id是一个整数因此用setInt方法

③ 参数create_date_time的值是根据当前时间得到的,create_date_time是时间类型因此用setDate方法

3.12 UserIdThreadLocal

public class UserIdThreadLocal {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void setUserId(Integer userId) {
        threadLocal.set(userId);
    }

    public static Integer getUserId() {
        return threadLocal.get();
    }

    public static void removeUserId() {
        threadLocal.remove();
    }

}

UserIdThreadLocal存放了userId,它包含static静态的ThreadLocal

3.13 UserIdFilter过滤器

@WebFilter("/*")
@Component
public class UserIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String userIdString = request.getHeader("userId");
        if(userIdString == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        }
        Integer userId = Integer.parseInt(userIdString);
        try {
            UserIdThreadLocal.setUserId(userId);
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            UserIdThreadLocal.removeUserId();
        }
    }

}

@WebFilter(“/*”)表示过滤所有请求,必须加@Component将过滤器对象注入到IOC容器中

3.14 SqlPrintInterceptor拦截器

@Intercepts({
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
})
@Component
@Log4j2
public class SqlPrintInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 目标:打印替换?的参数的sql语句
        int res = (int) invocation.proceed();
        Object[] args = invocation.getArgs();
        PreparedStatement preparedStatement = (PreparedStatement) args[0];
        MetaObject metaObject = SystemMetaObject.forObject(preparedStatement);
        preparedStatement = (PreparedStatement) metaObject.getValue("h.statement.delegate");
        String sql = preparedStatement.toString().substring(preparedStatement.toString().indexOf(":") + 1).replace("\n", " ");
        List<String> list = new ArrayList<>();
        for (String s : sql.split(" ")) {
            if("".equals(s)) {
                continue;
            }
            list.add(s.trim());
        }
        log.info(list.stream().collect(Collectors.joining(" ")));
        return res;
    }

}

SqlPrintInterceptor拦截器的目标是打印替换?的参数的sql语句,即实际sql执行语句,通过SystemMetaObject.forObject方法得到PreparedStatement对象

3.15 输出结果

在这里插入图片描述

可以看到拦截器修改sql成功

在这里插入图片描述

这是MyBatis输出的sql日志,可以看到拦截器添加参数值成功,sql中的?和参数值一一对应,其中参数类型已经解析完成

在这里插入图片描述

可以看到拦截器打印日志成功,实际执行的sql是将?替换成参数值,为了防止sql注入,字符串类型和日期类型都用单引号防止sql注入,这也是#{}可以防止sql注入的原因,整数类型没有加单引号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2073029.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人脸识别签到系统

人脸识别签到系统是一种利用计算机视觉技术和生物识别技术自动识别个体面部特征并进行身份验证的应用系统。这种系统通常应用于需要快速且准确的身份验证场景&#xff0c;例如公司员工打卡、学校上课签到、大型活动入场等。下面是对该系统的详细介绍&#xff1a; 项目背景及目…

【硬件模块】LCD1602显示模块

LCD液晶显示模块实物图 字符型液晶显示模块&#xff0c;可显示16列2行&#xff0c;共32个字符&#xff0c;每个字符都由5x8像素点阵构成。 常见型号&#xff1a;1602&#xff08;16列2行&#xff09;&#xff1b;2004&#xff08;20列4行&#xff09;&#xff1b;12864&#xf…

不用下载!玩《黑神话·悟空》图文教程by无影云电脑,开机即玩

使用阿里云无影云电脑畅玩《黑神话悟空》游戏图文教程&#xff0c;不需要下载&#xff0c;超简单开机即玩。无影云电脑提供WeGame版镜像和Steam版镜像&#xff0c;开机就能玩。阿小云分享阿里云官方发布的用无影云电脑畅玩《黑神话悟空》游戏图文教程&#xff1a; 无影云电脑玩…

笔记本电脑数据恢复的最佳解决方案 - 100%快速和安全

到目前为止&#xff0c;数字设备已成为我们生活中不可或缺的一部分。电脑、手机和数码相机丰富了我们的生活&#xff0c;给我们带来了很多便利。近年来&#xff0c;笔记本电脑越来越受到人们的欢迎&#xff0c;主要是因为它相对较轻且易于携带。 但是&#xff0c;如果笔记本电脑…

C++必修:set/map,mutiset/mutimap的用法

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 1. set的介绍 在 C 中&#xff0c;set 是一种关联容器&#xff0c;用于存储唯一的…

JavaScript初级——DOM查询的其他方法

1、在document中有一个属性 body &#xff0c;保存的是body的引用。 2、document.documentElement 保存的是 html 根标签。 3、document.all 代表页面中所有的元素。 4、根据元素的 class 属性值查询一组元素节点对象 getElementsByClassName&#xff08;&#xff09;可以根…

c++中的const权限及static成员

c中的const权限 void Print() {cout << _year << "-" << _mouth << "-" << _day << endl; } void f(const Date& d) {d.Print(); } this指针为非const的&#xff0c;故需要 //void Print(const Date* this) voi…

【机器学习】数据预处理、特征缩放以及有偏分布的基本概念

引言 数据预处理是机器学习过程中的一个关键步骤&#xff0c;它涉及对原始数据进行清洗、转换和重塑&#xff0c;以提高模型的性能和准确性 文章目录 引言一、数据预处理1.1 定义1.2 步骤1.2.1 数据清洗1.2.2 数据转换1.2.3 数据重塑1.2.4 数据分割1.2.5 数据增强1.2.6 处理不平…

Java—基础知识总结 ٩(๑❛ᴗ❛๑)۶

目录&#xff1a; 一、Java基础知识 1、Java的语言特性&#xff1a; 2、Java的注释&#xff1a; 3、标识符&#xff1a; 4、关键字&#xff1a; 5、数据类型与变量&#xff1a; 1&#xff09;、常量&#xff1a; 2&#xff09;数据类型&#xff1a; 3&#xff09;、变量…

零基础5分钟上手亚马逊云科技-搭建CDN加速应用访问

简介&#xff1a; 欢迎来到小李哥全新亚马逊云科技AWS云计算知识学习系列&#xff0c;适用于任何无云计算或者亚马逊云科技技术背景的开发者&#xff0c;通过这篇文章大家零基础5分钟就能完全学会亚马逊云科技一个经典的服务开发架构方案。 我会每天介绍一个基于亚马逊云科技…

标准版v5.4安卓手机小程序扫码核销读取不到核销码的问题

修改这个文件&#xff0c;红色的那块代码替换成绿色的这段代码&#xff0c;然后重新打包上传。 文件地址&#xff1a;template/uni-app/pages/admin/order_cancellation/index.vue let path decodeURIComponent(res.path); self.verify_code path.split(‘code’)[1]; h5…

MTK的ATA工厂PCBA贴片验证测试

1.ATA测试就是PCBA硬件测试,目的如下 在工厂生产过程中,在PCB SMT贴装器件并烧录软件后,需要验证DUT(device under test)板上各个功能模块单元的driver基本功能是否正常,通过测试筛查出贴片异常或元器件物料异常等不良PCBA主板。 2.要进行此测试,MTK官方所要求的条件如下…

【C++】函数模板特化:深度解析与应用场景

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

【25届秋招】Shopee 0825算法岗笔试

目录 1. 第一题2. 第二题3. 第三题 ⏰ 时间&#xff1a;2024/08/25 &#x1f504; 输入输出&#xff1a;LeetCode格式 ⏳ 时长&#xff1a;2h 本试卷有10道单选&#xff0c;5道多选&#xff0c;3道编程。 整体难度非常简单&#xff0c;博主20min成功AK&#xff0c;这里只给出编…

Self-attention反向传播和梯度消失计算

文章目录 1、前言2、Self-attention的特点3、为什么是(Q, K, V)三元组4、归一化和放缩4.1、Normalization4.2、Scaled4.3、总结 5、Softmax的梯度变化5.1、Softmax函数的输入分布是如何影响输出的5.2、反向传播的过程中的梯度求导5.3、出现梯度消失现象的原因 6、维度与点积大小…

如何使用ssm实现固定资产管理系统

TOC ssm167固定资产管理系统jsp 绪论 1.1 选题背景 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。计算机软件可以针对不同行业的营业特点以及管理需…

Golang测试func TestXX(t *testing.T)的使用

一般Golang中的测试代码都以xxx_test.go的样式&#xff0c;在命名测试函数的时候以Testxx开头。 以下是我写的一个单元&#xff1a; package testsimport "strings"func Split(s, sep string) (res []string) {i : strings.Index(s, sep)for i > -1 {res append…

常见虚拟现实硬件设备及特点

一、常见的虚拟现实设备介绍 &#xff08;1&#xff09;VR设备 &#xff08;2&#xff09;AR设备 &#xff08;3&#xff09;MR设备 二、各种虚拟现实设备的特点 序号种类设备沉浸感价格比较&#xff08;元&#xff09;占用场地等特点1VRPC主机普通显示器不是全沉浸6000就是办…

嵌入式UI开发-lvgl+wsl2+vscode系列:12、GUI Guider安装使用及在ssd202开发板上测试

一、前言 接下来我们根据开发板官方的指南安装lvgl的ui工具GUI Guider进行开发和测试。理论上还有SquareLine Studio&#xff0c;但是由于一些收费等因素暂时不做过多介绍&#xff0c;gui工具只是辅助&#xff0c;加快开发效率&#xff0c;很多时候还是得直接用代码写界面。&a…

睿考网:2024年中级经济师考试时间

报名时间&#xff1a;2024年8.12-9.11日 考试时间&#xff1a;2024年11.16-11.17日 成绩查询&#xff1a;2024年12月 报考条件&#xff1a; 1.高中毕业并取得初级经济专业技术资格&#xff0c;从事相关专业工作满10年; 2.具备大学专科学历&#xff0c;从事相关专业工作满6…