SqlSession 和 SqlSessionTemplate 简单使用及注意事项
1、SqlSession 简单使用
先简单说下 SqlSession 是什么?SqlSession 是对 Connection 的包装,简化对数据库操作。所以你获取到一个 SqlSession 就相当于获取到一个数据库连接,就可以对数据库进行操作。
SqlSession API 如下图示:
配置好数据,直接通过 SqlSessionFactory 工厂获取 SqlSession 示例,代码如下:
public class MyBatisCacheTest {private static SqlSessionFactory sqlSessionFactory;private static Configuration configuration;private static JdbcTransaction jdbcTransaction;private static Connection connection;private static MappedStatement mappedStatement;private static SqlSession sqlSession;static {try {InputStream inputStream = MyBatisCacheTest.class.getResourceAsStream("/mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);configuration = sqlSessionFactory.getConfiguration();configuration.setCacheEnabled(true);connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gwmdb?useSSL=false&allowPublicKeyRetrieval=true", "root", "itsme999");jdbcTransaction = new JdbcTransaction(connection);String statement = "org.apache.ibatis.gwmtest.dao.PersonMapper.getPerson";mappedStatement = configuration.getMappedStatement( statement);// 注意这里设置了自动提交sqlSession = sqlSessionFactory.openSession(true);} catch (Exception e) {e.printStackTrace();}}}
2、SqlSession 缓存使用
SqlSession 获取到后开始演示下它的缓存使用。代码如下:
public static void main(String[] args) throws Exception {PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);Person person1 = mapper.getPerson(1);System.out.println("person==person1 = " + (person == person1));}
最终结果输出为 true,因为在 SqlSession 里面是有缓存的,默认一级缓存开启,二级缓存不开启,这里暂时不讲二级缓存,想了解请看二级缓存使用篇。
但是在使用这个一级缓存时,需要注意,在多线程环境下面,会出现数据安全问题,多线程并发操作代码如下:
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {new Thread(() -> {// 准备好 10 个线程try {cdl.await();} catch (Exception e) {e.printStackTrace();}// 随便调用其中一个查询方法PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);System.out.println("person = " + person);}).start();cdl.countDown();}}
抛出异常如下:
### Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:155)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)at org.apache.ibatis.gwmtest.MyBatisCacheTest.lambda$main$0(MyBatisCacheTest.java:77)at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:163)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:137)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:90)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)... 5 more
具体原因是为什么呢?因为在多线程环境下面,共用同一个 SqlSession 导致的,具体原因看源码,SqlSession 底层调用 Executor,在 MyBatis 中它们是一对一关系。
在 MyBatis 中有分三个基本执行器:
- SimpleExecutor:每次数据库操作都需要重新编译 SQL 语句,然后开始操作数据库
- ResuExecutor (推荐):只有第一次访问数据库会编译 SQL 语句,后面不会重新编译,提高效率,然后操作数据库
- BatchExecutor:当需要批量操作数据库时,进行打包分批访问数据库
除了上面三个基本 Executor 之外,因为还有一些公共的操作,所以向上衍生出一个 BaseExecutor,比如最基本的一级缓存就是在这个执行器做的,因为一级缓存是本地缓存不能跨线程使用,所以又继续向上衍生出 CachingExecutor,二级缓存就是在这里做的,这里可以定义一些缓存比如:Redis、MongoDB 等等。
看到 SqlSession 操作一级缓存的地方(BaseExecutor 类中),源码如下:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...Object object = localCache.getObject(key);List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}return list;}private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...localCache.putObject(key, EXECUTION_PLACEHOLDER);List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);localCache.putObject(key, list);return list;}
假设现在两个线程并发调用 mapper.getPerson(1) ,最终都要拿到 SqlSession 实例去操作数据库。而 SqlSession 和 Executor 是一对一关系,SqlSession 最终会给到 BaseExecutor 处理,最终调用上面的源码 query() 方法。
而上面的源码你只需要关注两个地方:存和取缓存。存缓存的地方注意细节,MyBatis 会先往一级缓存中保存一个占位符 EXECUTION_PLACEHOLDER,具体作用是为了能够解决子查询中循环依赖问题,不展开叙述。注意这里保存的是占位符。假设现在线程1过来恰好往一级缓存中保存完这个占位符,但是线程1此时没来得及往下执行,CPU 执行权被线程2抢走,那么现在线程2过来执行 query() 方法,因为是同一个 SqlSession,所以 cacheKey 是一模一样的,线程2会去一级缓存中取值,此时线程2取出来的肯定是线程1之前在里面保存的占位符。线程1拿到这个占位符之后,开始执行类型转换,也就是对应这句代码:(List) localCache.getObject(key),你觉得此时泛型转换能成功么?肯定不能,所以直接抛出异常。
解决办法是什么?源码不太好改,只能从使用层面进行改进,主要是因为缓存 key 是一样的,线程1从缓存中可以取出一个占位符,那么让缓存 key 不一样不就行了么?最快最简单的让缓存 key 不一样就是换一个 SqlSession。用不同的会话去操作数据库是不会出现这样的问题。所以最终改进的代码如下:
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {new Thread(() -> {// 准备好 10 个线程try {cdl.await();} catch (Exception e) {e.printStackTrace();}// 调用查询方法sqlSession = sqlSessionFactory.openSession(true);PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);System.out.println("person = " + person);}).start();cdl.countDown();}}
就是每次都重新生成一个 SqlSession 实例。其实底层也换了一个 Connection 实例。这个就是我们常说的线程安全问题是 SqlSession 的一个实现 DefaultSqlSession,MyBatis 作者也对此类加以Note that this class is not Thread-Safe
的注释。
或者换个理解 SqlSesion 线程不安全,SqlSesion 是 Mybatis 中的会话单元,对于 Mybatis 中而言,一个会话对应一个 SqlSession,也对应一个JDBC中的 Connection。多个线程同时操作 Connection,A线程执行完 SQL,还想再执行点其他的,但是B线程对这个 Connection 进行commit 操作,导致A线程一脸懵逼。
2、SqlSessionTemplate 简单使用
上面 SqlSession 存在这样的安全问题,Spring 在继承它的时候,做了改进,在 SqlSession 上继续封装一层,具体是通过动态代理做的。SqlSessionTemplate 在每次调用 API 时都会重新给你创建 SqlSession 实例。这样就能保证每次都在不同的 SqlSession 会话中操作数据库,比较安全。
下面开始演示个问题,代码如下:
public static void main(String[] args) {PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);Payment payment = paymentMapper.queryAccount(1);Payment payment1 = paymentMapper.queryAccount(1);System.out.println("payment1 == payment = " + (payment1 == payment));}
最终输出结果为:false,和之前测试的结果不一样。SqlSession 不是有一级缓存嘛,为什么这里结果是 false。为什么?是因为 Spring 对 SqlSession 对象做了一层优化。之前说过同一个 SqlSession 在多线程环境下会出现安全问题,所以 Spring 在你每次操作 API 时都会重新创建新的 SqlSession 实例。所以 SqlSession 都是不一样的,就不用再去谈什么缓存。除非你是同一个 SqlSession 才有缓存之说。
那么怎么让一级缓存生效呢?可以开启事务,保证这些操作都在同一个事务下。改进代码如下:
public static void main(String[] args) {DataSourceTransactionManager tx = (DataSourceTransactionManager)context.getBean(TransactionManager.class);TransactionStatus transaction = tx.getTransaction(TransactionDefinition.withDefaults());PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);Payment payment = paymentMapper.queryAccount(1);Payment payment1 = paymentMapper.queryAccount(1);System.out.println("payment1 == payment = " + (payment1 == payment));tx.commit(transaction);}
最终结果为:true,进入 SqlSessionTemplate 核心源码如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;}LOGGER.debug(() -> "Creating a new SqlSession");session = sessionFactory.openSession(executorType);registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}
可以看到是从 TransactionSynchronizationManager 事务管理器中获取到一个 SqlSession 实例。如果没有开启事务,这个 TransactionSynchronizationManager 中获取不到,就会走下面的 openSession() 创建新的实例。
在看到 getResource() 方法,核心源码如下:
@Nullablepublic static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Object value = doGetResource(actualKey);if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +Thread.currentThread().getName() + "]");}return value;}@Nullableprivate static Object doGetResource(Object actualKey) {Map<Object, Object> map = resources.get();if (map == null) {return null;}Object value = map.get(actualKey);// Transparently remove ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {map.remove(actualKey);// Remove entire ThreadLocal if empty...if (map.isEmpty()) {resources.remove();}value = null;}return value;}
最终看到变量 resources 源码如下:
public abstract class TransactionSynchronizationManager {private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");
}
发现竟然是一个 ThreadLocal 变量,这是每个线程私有的东西,人手一份,互不影响,当你开启事务之后,这个变量就已经保存好一个 SqlSession 连接,所以每次调用 API 时获取到的都是同一个 SqlSession 对象,是同一个会话,那么一级缓存就会开始生效。如果你没有开启事务,就会通过 SqlSessionFactory 工厂调用 openSession() 方法打开 SqlSession 会话,但是此时 SqlSessionTemplate 每次都会通过 SqlSessionFactory 打开一个新的 SqlSession,这样就不存在说啥一级缓存了都,完全两个 SqlSession。
相关文章:
SqlSession 和 SqlSessionTemplate 简单使用及注意事项
1、SqlSession 简单使用 先简单说下 SqlSession 是什么?SqlSession 是对 Connection 的包装,简化对数据库操作。所以你获取到一个 SqlSession 就相当于获取到一个数据库连接,就可以对数据库进行操作。 SqlSession API 如下图示:…...
1. QSaveFile和QFile的简单使用
1. 说明 QSaveFile和QFile两个类都是用来操作文件的,区别在于QSaveFile在对文件进行写入时有一种保护机制,再写入出错时,不会对源文件中的内容进行操作。该类在执行写操作时,会先将内容写入到一个临时文件中,如果没有…...
工业4.0是如何优化垃圾处理行业的
如今,工业4.0正在影响着制造业和物流等行业,其发展潜力在未来还有望进一步扩大。一些全球领先的垃圾处理公司已经开始在水处理和废物回收等领域应用工业4.0。工业4.0的创新给这个领域带来了一些必要的改进。随着环境危机的加剧,垃圾处理行业面…...
vue 动画(transition)
一、 实现原理 在插入、更新、移除 DOM 元素时,在合适的时候给元素添加样式类名,配合 CSS 样式使用,实现动画效果。 通俗来讲,就是将要进行动画操作的 DOM 元素用 transition 标签包裹起来。在此html元素运动前,运动…...
Python 爬虫工程师面试经验分享,金三银四
🙃 作为一个 Python 爬虫工程师,我可以分享一些我在面试中的经验和建议。 首先一点是在面试中要表现自信、友好、乐于合作,同时对公司的业务和文化也要有一定的了解和兴趣,这些也是公司在招聘中看重的因素。 文章目录🕛…...
MySQL实战篇-MySQL 降配导致的实例宕机
问题描述 由于近期对服务器进行了降配,该mysql数据库会进行批量写入操作,直接导致实例宕机 查看错误日志: 2021-02-02T09:09:23.557505Z 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 16791ms. The settings might not be optimal. (fl…...
时隔多年,这次我终于把动态代理的源码翻了个地儿朝天
本文内容整理自 博学谷狂野架构师 动态代理简介 Proxy模式是常用的设计模式,其特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。 用户可以更加结构图࿰…...
数据分析-深度学习 Tensorflow Day6
我们需要解决的问题:1: 什么是bp 神经网络?2:理解bp神经网络需要哪些数学知识?3:梯度下降的原理4: 激活函数5:bp的推导。1.什么是bp网络?引用百度知道回复:“我们最常用的…...
leaflet 设置多个marker,导出为一个geojson文件(066)
第066个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中使用L.marker设置多个markers, 通过数据重组,导出为geojson文件。 这里面 ayer instanceof L.Marker 是一个很重要的判断条件,可以灵活地去运用。 直接复制下面的 vue+openlayers源代码,操作2分钟即可…...
企业与第三方供应商合作时,会存在哪些安全风险?
随着现代社会的发展,企业供应链、产业供应链已日渐成熟。其中,供应商与企业的关系也由最初的纯粹买卖关系发展成了合作伙伴关系。在整个供应链体系中,供应商与其受众承担着供应链中环环相扣的责任,可以说,企业安全的薄…...
技术源自洛克希德·马丁,光场XR眼镜FYR解析
专注于医疗场景的一家XR眼镜厂商FYR(全称:FYR Medical)近期亮相,并宣布完成了260万美元A轮融资,本轮融资由NuVasive领投,资金将用于开发世界上第一个XR光场“放大镜”类产品。据青亭网了解,NuVa…...
剑指 Offer 10- II. 青蛙跳台阶问题(LeetCode 70. 爬楼梯)(动态规划打表)
题目: 链接:剑指 Offer 10- II. 青蛙跳台阶问题;LeetCode 70. 爬楼梯 难度:简单 相关博文:剑指 Offer 10- I. 斐波那契数列(动态规划打表) 一只青蛙一次可以跳上1级台阶,也可以跳上…...
webpack(高级)--文件的压缩Terser(js/css/html) Tree Shaking
webpack Terser Terser是一个javascript的解释(Parser),Mangler(绞肉机) /Compressor(压缩机)的工具集 早期我们会使用uglify-js来压缩,丑化我们的javascript代码 但是目前已经不在维护 并且不支持ES6语法 Terser是从uglify-es fork 过来的 也就是说 Terser可以帮…...
做软文发布需要注意哪些细节?
软文发布是一种有效的网络营销和推广活动,它以媒体等形式把产品信息植入到软文报道或新闻中,进行心理暗示和引导销售,进行正面宣传以及促进销售的新型网络营销方式,它不但能够有效地推行产品宣传、也能有效地提高网络曝光率&#…...
【Python】一篇文章读懂yield基本用法
这一次,田辛老师想通俗易懂地解释一下Python中的yield功能。 本文要说明以下四个问题: yield是什么什么是迭代器和生成器yield的基本用法如何使用yield from 用真正简单的方法讲解yield并不容易。 我想,就算你不懂yield语句,也…...
Docker getting started
系列文章目录 Docker 概述 Docker getting started 文章目录系列文章目录前言一、容器及镜像的概念二、容器化一个应用三、更新应用四、分享应用五、持久化数据存储volume mount 和 bind mount比较Container volumesbind mounts六、跨多容器的应用七、Docker 其它八、Docker 图…...
【Uniapp使用遇到问题合集】
Uniapp使用遇到问题合集问题一跳转页面后无法进行滑动/滚动的操作描述解决方法问题一 跳转页面后无法进行滑动/滚动的操作 描述 如题,实际操作是我在uniapp自带的组件uni-popup弹出层中加入了一个点击事件,点击后可跳转到指定的页面 但实际运行中出现了跳转过后页面过长时无…...
宝塔面板破解最新教程
宝塔,让运维简单高效。面板支持Linux与Windows系统。一键配置:LAMP/LNMP、网站、数据库、FTP、SSL,通过Web端轻松管理服务器。今天考高分网就简单说一下BT宝塔面板专业版最新破解教程。 网地址:https://www.bt.cn/ 网上的破解版一般分为两种,一种是直接…...
基于zookeeper的Hadoop集群搭建详细步骤
目录 一、一些基本概念 二、集群配置图 三、Hadoop高可用集群配置步骤 1.在第一台虚拟机解压hadoop-3.1.3.tar.gz到/opt/soft/目录 2.修改文件名、属主和属组 3.配置windows四台虚拟机的ip映射 4.修改hadoop配置文件 (1)hadoop-env.sh (2)workers (3)crore-site.xml …...
职称有哪些意义?如何提升职称?
每年我们会看到很多人都会努力地提升自己的职称,那么为什么大家都想要晋升职称?在这里余老师说说他的作用,您可以参考一下。 一、个人金钱方面的提升 工资。职称直接关联的就是涨工资了。正常情况下,职称和工资是一一对应的了,…...
mulesoft MCIA 破釜沉舟备考 2023.02.15.09
mulesoft MCIA 破釜沉舟备考 2023.02.15.09 1. According to MuleSoft, which deployment characteristic applies to a microservices application architecture?2. Refer to the exhibit.3. Mule application A receives a request Anypoint MQ message REQU with a payload…...
【项目实战】@ConditionalOnProperty注解让我少写了一些if判断
一、需求说明 本机启动含有XXL-job的工程,发现每次都会进行XXL-job的init的动作。这会导致本机每次启动都会把自己注册到XXL-job的服务端。但是我明明本地调试的功能不想要是编写定时任务,于是想了下,是否可以设计一个开关,让本机…...
SQL中的游标、异常处理、存储函数及总结
目录 一.游标 格式 操作 演示 二.异常处理—handler句柄 格式 演示 三.存储函数 格式 参数说明 演示 四.存储过程总结 一.游标 游标(cursor)是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、…...
Splashtop:支持M1/M2芯片 Mac 电脑的远程控制软件
M1和M1芯片的Mac电脑现在越来越多了。M1和M2的强大性能,让使用者们办公、娱乐如虎添翼。 M1 芯片于2020年11月11日推出,是Apple 首款专为Mac打造的芯片,拥有格外出色的性能、众多的功能,以及令人惊叹的能效表现。M1 也是Apple 首款…...
实验十三、阻容耦合共射放大电路的频率响应
一、题目 利用 Multism 从以下几个方面研究图1所示的阻容耦合共射放大电路的频率响应。图1阻容耦合共射放大电路图1\,\,阻容耦合共射放大电路图1阻容耦合共射放大电路(1)设 C1C210μFC_1C_210\,\textrm{μF}C1C210μF,分别测试它们所确定…...
【每天进步一点点】函数表达式和函数声明
函数声明 function 函数名(){} 函数声明会被率先读取。 函数声明后不会立即执行,会在我们需要的时候调用到。 由于函数声明不是一个可执行语句,所以不以分号结束。 函数表达式 表达式赋值给了一个变量 const 变量名 functi…...
JavaScript void
文章目录JavaScript voidjavascript:void(0) 含义href"#"与href"javascript:void(0)"的区别JavaScript void javascript:void(0) 含义 我们经常会使用到 javascript:void(0) 这样的代码,那么在 JavaScript 中 javascript:void(0) 代表的是什么…...
笔记本电脑怎么连接无线网wifi?不同电脑系统的使用教程(2023最新)
现在越多人使用笔记本电脑,在我们的日常生活和工作中是很难离开它的。想要更快速地上网,我们都会选择连接无线网的wifi。有时笔记本电脑无法连接网络,你知道这是什么原因吗?笔记本电脑怎么连接无线网wifi?方法很简单&a…...
从lettcue插件看skywalking
lettcue 的写操作是异步的。io.lettuce.core.RedisChannelWriter.write进行写入,io.lettuce.core.protocol.RedisCommand进行异步读取数据 skywalking 插件大体逻辑 在方法执行前,通过ContextManager创建span创建span的同时,判断trace上下文…...
explain 每个列的含义
官网传送门:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html 实例表 DROP TABLE IF EXISTS actor;CREATE TABLE actor (id int(11) NOT NULL,name varchar(45) DEFAULT NULL,update_time datetime DEFAULT NULL,PRIMARY KEY (id)) ENGINEInnoDB DEFA…...
中小型网站建设与管理设计总结/2023年广州疫情最新消息
如何显示文件夹中的图片,数据库表中,只记录图片名称或是图片路径。写一个用户控件,即轻易把图片显示出来。 另外,如何取得记录的主键,您是用HiddenField,DataKeyNames还是用DataRowView来取得? …...
wordpress语言切换 seo/品牌广告视频
索引是帮助mysql获取数据的数据结构。最常见的索引是Btree索引和Hash索引。不同的引擎对于索引有不同的支持:Innodb和MyISAM默认的索引是Btree索引;而Mermory默认的索引是Hash索引。Hash索引所谓Hash索引,当我们要给某张表某列增加索引时&…...
什么自己做网站吗/代做关键词收录排名
1、JSP页面 <a href"javascript:void();" id"PicName" > 选择图片</a> <input type"file" id"fileToUpload" name"fileToUpload" ><!--需要设置name属性值,不然上传不了 --> <butto…...
做网站高流量赚广告费/北京seo优化方案
概述 什么是分库分表 数据数量是不可控的,随着时间和业务发展,造成表里面数据越来越多,如果再去对数据库表CURD操作时,就会有性能问题。 解决方案 为了解决由于数据量过大而造成数据库性能降低问题,主要有下面两种…...
网站建设如何不被忽悠/各大网站收录查询
1.Intent:一个Intent就是对一次将要执行的操作的抽象描述 2.拨打电话,发送短信 3.启动新的Activity,传递参数和返回参数...
营销策略的重要性/seo搜索引擎是什么意思
信息技术学院拟选拔 35 名学生参加第十一届蓝桥杯省赛,具体名额分配安排如下:1、电子类赛项以教研室为单位选拔 4 人参加单片机设计与开发。2、软件类比赛以学院为单位组织校级模拟赛选拔产生,根据学生报名赛项人数比例分配名额。本届软件类赛…...