Mybatis(3) web项目

web项目

  • 1、准备
  • 2、分析
  • 3、 MyBatis对象作用域以及事务问题
  • 4、问题

实现一个转账系统

1、准备

①准备一个web模块 在这里使用了maven archetype,选择web
之后会生成 一个web模块,但是不同的版本可能不同,在这里我就没有java和resources目录,记得自己创建一个。
②之后导入依赖,因为是mybatis,肯定要导入mybatis和mysql依赖,web项目,导入servlet依赖。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>web-app01</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>web-app01 Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <!--  mybatis-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.10</version>
    </dependency>
    <!--    mysql-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
    </dependency>
    <!--    logback-->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.11</version>
    </dependency>
    <!--    servlet-->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.30</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>web-app01</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

③web.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0"
         metadata-complete="false">

</web-app>

注意 metadata-complete 这个属性 如果为true的话意味着只支持 web.xml 文件,不支持注解。
④准备其他配置文件
首先看一下我的项目目录
在这里插入图片描述
jdbc.properties

jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.name=ckytest
jdbc.pwd=123456

logbackxml

<?xml version="1.0" encoding="UTF-8"?>

<configuration debug="false">
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!--mybatis log configure-->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties url="file:///D:/jdbc.properties"></properties>
<!--一个数据库对应一个一个环境,一个数据库对应一个sqlSessionFactory对象-->
<!--    即一个环境 对应一个sqlSessionFactory对象-->
<!--    默认 使用的是defalut的数据库-->
    <environments default="mybatisDB">

        <environment id="mybatisDB">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.name}"/>
                <property name="password" value="${jdbc.pwd}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--sql映射文件创建好之后,需要将该文件路径配置到这里-->
        <mapper resource="ActMapping.xml"/>
    </mappers>
</configuration>

ActMapping.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">
<!--namespace先随意写一个-->
<mapper namespace="aaa">

<select id="selectAct" resultType="com.cky.beans.Account">
    select * from t_act where actno=#{actno};
</select>

    <update id="updateAct">
        update t_act set balance = #{balance} where actno = #{actno}
    </update>
</mapper>

⑤准备java类
使用MVC模式
在这里插入图片描述
不全放出来了,只写几个重要的。
工具类

package com.cky.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

public class MysqlsessionUtils {
    private static SqlSessionFactory sqlSessionFactory;

    /**
     * 类加载时初始化sqlSessionFactory对象
     */
    static {
        try {
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
            sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static ThreadLocal<SqlSession> local = new ThreadLocal<>();

    /**
     * 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。
     *
     * @return 新的会话对象
     */
    public static SqlSession getSqlSession() {
        SqlSession sqlSession = local.get();
        if (sqlSession == null) {
            sqlSession = sqlSessionFactory.openSession();
            local.set(sqlSession);
        }
        return sqlSession;
    }


    public static void closeSession(){
        SqlSession sqlSession = local.get();
        if (sqlSession != null) {
            sqlSession.close();
        }
        local.remove();
    }}

web层

package com.cky.web;

import com.cky.beans.Account;
import com.cky.exception.AppException;
import com.cky.exception.MoneyNotEnoughException;
import com.cky.service.ActService;
import com.cky.service.impl.ActServiceimpl;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
@WebServlet(value = "/transfer")
public class ActServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String fromActno = req.getParameter("fromActno");
        String toActno = req.getParameter("toActno");
        double money = Double.parseDouble(req.getParameter("money"));
        //业务层  转帐逻辑
        ActService actService=new ActServiceimpl();

        //视图层
//        resp.sendRedirect();
        try {
            actService.transfer(fromActno,toActno,money);
            resp.sendRedirect(req.getContextPath()+"/sussess.html");
        } catch (MoneyNotEnoughException e) {
            resp.sendRedirect(req.getContextPath()+"/error.html");
        } catch (AppException e) {
            resp.sendRedirect(req.getContextPath()+"/error.html");

        }
        

    }
}

Serviceimpl

package com.cky.service.impl;

import com.cky.beans.Account;
import com.cky.dao.Actdao;
import com.cky.dao.impl.ActDaoimpl;
import com.cky.exception.AppException;
import com.cky.exception.MoneyNotEnoughException;
import com.cky.service.ActService;
import com.cky.util.MysqlsessionUtils;
import org.apache.ibatis.session.SqlSession;

public class ActServiceimpl implements ActService {

    Actdao actdao=new ActDaoimpl();

    private Actdao accountDao = new ActDaoimpl();

    @Override
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
        // 查询转出账户的余额
        Account fromAct = accountDao.queryAct(fromActno);
        if (fromAct.getBalance() < money) {
            throw new MoneyNotEnoughException("对不起,您的余额不足。");

        }
        SqlSession sqlSession=null;
        try {
            // 程序如果执行到这里说明余额充足
            // 修改账户余额
            Account toAct = accountDao.queryAct(toActno);
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);
            // 更新数据库(添加事务)
           sqlSession = MysqlsessionUtils.getSqlSession();
            accountDao.actUpdate(fromAct);
            // 模拟异常
            String s = null;
            s.toString();
            accountDao.actUpdate(toAct);
            sqlSession.commit();

        } catch (Exception e) {
            sqlSession.rollback(); // 回滚事务
            throw new AppException("转账失败,未知原因!");

        }
        finally {
            MysqlsessionUtils.closeSession();  // 只修改了这一行代码。
        }

    }
}

2、分析

1
首先是
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
这是相当于是一个map集合,key是线程的名字,value是该线程要存储的内容,在这里就是一个连接,一个线程对应一个连接。get()是根据当前的线程获取连接,set()是给当前的线程设置连接。
2
在弄这个简单的web项目时,出来一个很大的bug,就是我的表,我用的是MyISAM存储引擎,该引擎默认就不支持事务,所以即使我把使Autocommit设置为关闭,MyISAM表仍然无法支持事务,因为它本身不支持事务的概念。所以要用INnoDB引擎啊!!!注意。
3
在这里,我模拟了一个异常
// 模拟异常
String s = null;
s.toString();
就是看当异常出现时,会不会导致一条记录更新,一条记录不更新。这也是为什么要用事务的原因,记得,事务一定要在一个连接里,而一个线程也对应一条连接
4
dao层就是专门用来与数据库连接的,而业务层就是专门做业务逻辑,web层则是像个司令官,收集到请求的信息后,要求service层来实现业务,之后调用视图层将内容返还给用户。

3、 MyBatis对象作用域以及事务问题

MyBatis核心对象的作用域
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式。

4、问题

DAO层实现类

package com.cky.dao.impl;

import com.cky.beans.Account;
import com.cky.dao.Actdao;
import com.cky.util.MysqlsessionUtils;
import org.apache.ibatis.session.SqlSession;

public class ActDaoimpl implements Actdao {


    @Override
    public Account queryAct(String act) {
        SqlSession sqlSession = MysqlsessionUtils.getSqlSession();
        Account selectAct = (Account) sqlSession.selectOne("aaa.selectAct", act);
        return selectAct;

    }

    @Override
    public int actUpdate(Account account) {
        SqlSession sqlSession = MysqlsessionUtils.getSqlSession();
        int update = sqlSession.update("aaa.updateAct", account);
        return update;
    }
}

我们不难发现,这个dao实现类中的方法代码很固定,基本上就是一行代码,通过SqlSession对象调用insert、delete、update、select等方法,这个类中的方法没有任何业务逻辑,既然是这样,这个类我们能不能动态的生成,以后可以不写这个类吗?答案:可以。
之后再讲~~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

缺陷检测项目 | 使用小数据集训练实现锅炉水冷壁管表面视觉缺陷检测

项目应用场景 面向锅炉水冷璧管表面视觉缺陷检测场景&#xff0c;项目支持训练&#xff0c;使用小数据集就能够实现很好的缺陷检测效果。 项目效果&#xff1a; 项目细节 > 具体参见项目 README.md (1) 安装依赖&#xff0c;包括 gcForest、AutoKeras&#xff0c;然后安装其…

JUC并发编程—— 对volatile的理解及DCL的解决方法

volatile的原理 volatile 的底层实现原理是内存屏障&#xff0c;Memory Barrier&#xff08;Memory Fence&#xff09; 加入 volatile 关键字后&#xff0c;写指令&#xff08;被 volatile 修饰的变量在对此变量修改时&#xff09;会加入写屏障&#xff0c;读指令&#xff08…

超图打开不同格式的dem文件

dem&#xff0c;数字高程模型&#xff1b; dem文件的后缀是什么? 有*.dem格式的&#xff0c;也有Raster&#xff0c;ASCII和Tiff类型的。Raster类型的是一个raster文件夹里面有很多不同格式的文件共同组成了DEM文件的内容。ASCII类型的是个txt文件。Tiff类型的也是一个文件夹…

[leetcode] 637. 二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[3.00000,14.50000,11.00000] 解释&#xff1a;第 0 层的平均值…

uniapp打包

1.小程序 需要对应上appId才可以上传还要成为该小程序的开发者开可以点击上传 2.APP打包 点击发行选择云打包&#xff0c;会弹出下方的窗口 如果公司没有给证书信息&#xff0c;就去香蕉云编去申请默认的&#xff0c;把证书文件下载出来设置路径

文献学习-24-用于少发罕见病诊断的动态特征拼接

Dynamic feature splicing for few-shot rare disease diagnosis Authors: Yuanyuan Chen, Xiaoqing Guo , Yongsheng Pan , Yong Xia , Yixuan Yuan Source: Medical Image Analysis 90 (2023) 102959 Keywords: 少样本学习 罕见病诊断 transformer 特征拼接 通道相似度 Ab…

k8s练习-创建一个Deployment

创建Deployment 创建一个nginx deployment [rootk8s-master home]# cat nginx-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata:name: nginx-deployment spec:selector:matchLabels:app: nginx # 配置pod的labelsreplicas: 2 # 声明2个副本template:metada…

开发组合:PHP+MySQL 同城社区小程序源码 同城便民信息发布系统源码 源码开源可二开含搭建教程

同城便民信息发布系统源码在提升信息发布效率、促进商家宣传、增强用户互动、实现信息聚合与分类管理、个性化定制与扩展以及数据统计与分析等方面发挥着重要作用。 今天小编给大家分享一个同城社区小程序源码、同城便民信息发布系统源码&#xff0c;开发组合PHPMySQL&#xf…

每天学点儿Python(3) -- for循环

for循环结构格式如下 for 循环变量 in 遍历对象:语句块 举例一、 for i in "Hello"print(i) 执行结果如下 举例二、 #打印100-999之间的水仙花数 #注意&#xff1a;Python中 / 除法&#xff0c;运输后为浮点数, // 为取除法后的整数&#xff0c;而不是C/C中的注释…

C++之调用Python

1、配置头文件 Python安装目录下的include目录加入头文件目录。Visual Studio2022中操作路径是&#xff1a;属性–> C/C -> 常规-> 附加包含目录 C:\Users \AppData\Local\Programs\Python\Python39\include 2、配置lib库目录 要将Python39.lib加入编译链接。Visua…

【考研数学】汤家凤基础+武忠祥强化,如何高效衔接?看这一篇!无标题】

目标分在100的话&#xff0c;建议在备考初期就把分数定在120&#xff01; 因为如果一开始就没有按高分备考&#xff0c;可能最后只能考到七八十&#xff0c;备考就尽力比自己的目标分在多高20分去准备。 本人属于基础很差相当于是零基础的考研党&#xff0c;经过一年备考成功…

SAP 修改消息号处理简介

在项目经常会遇到更改SAP消息号的地方,从警告的消息W变化报错的消息E。 通常通过TCODE:SE91来查询和创建消息号 关于消息号的几个后天表 T100:包含所有的message T100C:你定义的message通常将出现在此表 T100s:Configurable system messages顾名思义就是你能设置的消息. M…

北京WordPress建站公司

北京wordpress建站&#xff0c;就找北京wordpress建站公司 http://wordpress.zhanyes.com/beijing

代码随想录 Day31 贪心算法 理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和

目录 理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和 理论基础 1. 什么是贪心&#xff1a; 就是通过局部最优搜索全局最优 2. 贪心的两个极端&#xff1a;要么就是很简单的常识&#xff0c;要么就是很难理解的。 3. 贪心算法没有套路可言就是找局部最优&#xf…

SD-WAN组网面临的安全挑战?如何提供有效的安全措施

SD-WAN&#xff08;软件定义广域网&#xff09;技术的广泛应用&#xff0c;企业面临着越来越多的网络安全挑战。尽管SD-WAN带来了灵活性和效率的提升&#xff0c;但其开放性和基于云的特性也带来了一系列安全威胁。本文将探讨SD-WAN组网面临的安全挑战&#xff0c;并提供一些有…

如何设计一个能够支持高并发的系统?

一、问题解析 设计一个能够支持高并发的系统需要考虑多方面的因素&#xff0c;包括架构、性能优化、容错和可伸缩性等。以下是一些一般性的建议和实践&#xff1a; 1、分布式架构&#xff1a;将系统分解成多个模块&#xff0c;采用分布式架构来降低单点故障的风险&#xff0c…

DXP学习3-单片机时钟显示系统的层次原理图设计

目录 一&#xff0c;自上而下的子母图设计 1&#xff0c;绘制层次式电路母图 1)工程及原理图创建和保存 2)开始绘制层次式母图main.SchDoc 2&#xff0c;绘制图纸符号 1&#xff09;properties选项卡 2&#xff09;designator标号 3&#xff09;filename文件名 4&…

VBA技术资料MF136:复制整个数据范围到PowerPoint

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

稻盛和夫|普通人如何才能取得非凡成就?

哈喽,你好啊,我是雷工! 稻盛和夫老先生曾经回答过这么一个问题: 资质平庸的普通人如何才能取得非凡的成就? 稻盛和夫认为:人生成就=能力努力态度。 也就是:做一个努力工作却不甘于只做眼前的事,而想要做更有挑战的事,这种人才能逃离平庸,取得非凡成就。 01 不甘平凡…

【嵌入式系统设计师】软考2024年5月报名流程及注意事项

2024年5月软考嵌入式系统设计师报名入口&#xff1a; 中国计算机技术职业资格网&#xff08;http://www.ruankao.org.cn/&#xff09; 2024年软考报名时间暂未公布&#xff0c;考试时间上半年为5月25日到28日&#xff0c;下半年考试时间为11月9日到12日。不想错过考试最新消息…
最新文章