javaee之spring3
模拟一个银行转账事务
先来看一下基础文件
先来看这个spring中的bean.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 配置Service --><bean id="accountService" class="com.pxx.service.impl.AccountServiceImpl"><!-- 注入dao --><property name="accountDao" ref="accountDao"></property></bean><!--配置Dao对象--><bean id="accountDao" class="com.pxx.dao.impl.AccountDaoImpl"><!-- 注入QueryRunner --><property name="runner" ref="runner"></property></bean><!--配置QueryRunner--><bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"><!--注入数据源--><constructor-arg name="ds" ref="dataSource"></constructor-arg></bean><!-- 配置数据源 --><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"><!--连接数据库的必备信息--><property name="driverClass" value="com.mysql.jdbc.Driver"></property><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property><property name="user" value="root"></property><property name="password" value="1234"></property></bean>
</beans>
现在来个一Service的实现类和一个dao的实现类
AccountServiceImpl.java
package com.pxx.service.impl;import com.pxx.dao.IAccountDao;
import com.pxx.domain.Account;
import com.pxx.service.IAccountService;import java.util.List;/*** 账户的业务层实现类*/
public class AccountServiceImpl implements IAccountService {private IAccountDao accountDao;public void setAccountDao(IAccountDao accountDao) {this.accountDao = accountDao;}public List<Account> findAllAccount() {return accountDao.findAllAccount();}@Overridepublic Account findAccountById(Integer accountId) {return accountDao.findAccountById(accountId);}public void saveAccount(Account account) {accountDao.saveAccount(account);}public void updateAccount(Account account) {accountDao.updateAccount(account);}public void deleteAccount(Integer acccountId) {accountDao.deleteAccount(acccountId);}
}
AccountDaoImpl.java
package com.pxx.dao.impl;import com.pxx.dao.IAccountDao;
import com.pxx.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;import java.util.List;/*** 账户的持久层实现类*/
public class AccountDaoImpl implements IAccountDao {private QueryRunner runner;public void setRunner(QueryRunner runner) {this.runner = runner;}@Overridepublic List<Account> findAllAccount() {try{return runner.query("select * from account",new BeanListHandler<Account>(Account.class));}catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic Account findAccountById(Integer accountId) {try{return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);}catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic void saveAccount(Account account) {try{runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());}catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic void updateAccount(Account account) {try{runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());}catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic void deleteAccount(Integer accountId) {try{runner.update("delete from account where id=?",accountId);}catch (Exception e) {throw new RuntimeException(e);}}
}
现在我们要去service里面增加一个方法,来进行银行业务转账
测试一下我们的配置
上面测试方法一旦执行,aaa账户就会-100,然后bbb账户就会+100
先来看原始账户
然后调用测试方法
但是这个代码很容易出现一个问题,那就是如果这段代码中间出现一个异常呢
先来看一下原始账户
果然报了一个算术异常
这里已经违背了数据的原子性(Atomicity) ,事务要么都完成,要么都失败,同时也违背了一致性(consistency)事务前后数据总量必须保持一致。
分析事务问题,并编写ConnectionUtils工具
先拿一张图来说明一下
问题的出现,在于,我们调用了四个连接对象,所以每一个连接对象都代表了一个事务。我们现在必须想办法把这个四个事务变成一个事务,也就是说把四个Connection连接对象变成一个Connection连接对象
这里的做法就是把这个Connection对象进行事务绑定到一个线程上
package com.pxx.utils;import javax.sql.DataSource;
import java.sql.Connection;/*** 连接工具类,用于从数据源中获取一个连接,并且实现和线程绑定*/
public class ConnectionUtils {private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();//这里采用属性类型注入private DataSource dataSource;public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}/*** 获取当前线程上的连接*/public Connection getThreadConnection() {try{//1.先从ThreadLocal上获取Connection conn = tl.get();//2.判断当前线程上是否有连接if (conn == null) {//3.从数据源中获取一个连接,并且存入ThreadLocal中conn = dataSource.getConnection();tl.set(conn);}//4.返回当前线程上的连接return conn;}catch (Exception e){throw new RuntimeException(e);}}
}
那么下面我们用动态代理解决一下事务的问题
来说一下思路:
一、我们先来写一个事务管理工具
TransactionManager.java
package com.pxx.utils;/**** 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接*/
public class TransactionManager {//引入一个连接对象private ConnectionUtils connectionUtils;//我们要采用set注入,必须添加一个set方法public void setConnectionUtils(ConnectionUtils connectionUtils) {this.connectionUtils = connectionUtils;}//开启事务public void beginTransaction() {try {connectionUtils.getThreadConnection().setAutoCommit(false);}catch (Exception e){e.printStackTrace();}}/*** 提交事务*/public void commit(){try {connectionUtils.getThreadConnection().commit();}catch (Exception e){e.printStackTrace();}}/*** 回滚事务*/public void rollback(){try {connectionUtils.getThreadConnection().rollback();}catch (Exception e){e.printStackTrace();}}/*** 释放连接*/public void release(){try {connectionUtils.getThreadConnection().close();//还回连接池中connectionUtils.removeConnection();}catch (Exception e){e.printStackTrace();}}
}
然后添加这个代理工厂BeanFactory.java
package com.pxx.factory;import com.pxx.service.IAccountService;
import com.pxx.utils.TransactionManager;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class BeanFactory {private IAccountService accountService;private TransactionManager txManager;public void setTxManager(TransactionManager txManager) {this.txManager = txManager;}public final void setAccountService(IAccountService accountService) {this.accountService = accountService;}/*** 把service里面的方法全部进行增强*/public Object getInstance() {return Enhancer.create(accountService.getClass(), new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {//这里面就是具体增强代码try {Object value = null;//主要是我们需要在执行一个方法前后添加事务控制txManager.beginTransaction();//开启事务value = method.invoke(accountService,args);//有啥参数就拿过来匹配//提交事务txManager.commit();return value;} catch (Exception e) {//有异常出现,把数据进行回滚txManager.rollback();;throw new RuntimeException(e);} finally {//释放连接txManager.release();}}});}
}
然后去配置一下bean.xml各个对象之间的依赖关系
上一下这个完整的配置文件
bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><!--配置BeanFactory--><bean id="beanFactory" class="com.pxx.factory.BeanFactory"><!--注入数据类型--><property name="accountService" ref="accountService"></property><property name="txManager" ref="txManager"></property></bean><!--配置一下代理的service这个是通过某个对象的方法得到的--><bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getInstance"></bean><!-- 配置Service --><bean id="accountService" class="com.pxx.service.impl.AccountServiceImpl"><!-- 注入dao --><property name="accountDao" ref="accountDao"></property></bean><!--配置Dao对象--><bean id="accountDao" class="com.pxx.dao.impl.AccountDaoImpl"><!-- 注入QueryRunner --><property name="runner" ref="runner"></property></bean><!--配置QueryRunner--><bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"><!--注入数据源--><constructor-arg name="ds" ref="dataSource"></constructor-arg></bean><!-- 配置数据源 --><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"><!--连接数据库的必备信息--><property name="driverClass" value="com.mysql.jdbc.Driver"></property><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property><property name="user" value="root"></property><property name="password" value="5201314"></property></bean><!--配置Connection工具类 ConnectionUtils--><bean id="connectionUtils" class="com.pxx.utils.ConnectionUtils"><!--它需要一个数据源对象--><property name="dataSource" ref="dataSource"></property></bean><!--配置事务管理器--><bean id="txManager" class="com.pxx.utils.TransactionManager"><!--注入ConnectionUtils工具类--><property name="connectionUtils" ref="connectionUtils"></property></bean></beans>
然后去看一下测试文件AccountServiceTest
那么原因还是在于
下面看一下test测试方法
在去测试一下业务,先看一下没出异常之前,会不会出问题
测试之前的数据表
测试之后的数据表
正常操作
现在给这个业务添加一个异常
异常有了,看看数据啥情况
我说一下在进行数据rollback之前,必须确认客户端的事务提交是如下状态,也就是关闭自动提交
来看一下原始数据
异常出现
数据没有变化
面向切面编程AOP
Spring中的AOP
1.AOP的相关术语
连接点:
什么叫切入点
就是对拦截点的增强的方法,所以切入点一定是拦截点,但是拦截点不一定是切入点,很可能我们只是对某一个普通方法进行拦截,但并没有增强具体的业务功能
现阶段没有用处,了解一下
就是给目标对象动态增加功能的过程
这个就是一个编码过程
spring中基于XML的AOP具体配置步骤
在使用这个AOP配置之前,现在maven里面导入一个依赖jar包
上面这个就是给我们解析切入点表示式的jar包
1、把通知Bean也交给spring来管理
2、使用aop:config标签表明开始AOP的配置
3、使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
4、在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前执行:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
比如现在utils包下面有一个类
然后我们去看一下业务层,有如下的方法
具体配置信息
四种常用通知类型
先在logger.java里面看几个配置好的方法
然后再去看一下bean.xml的配置
上面执行效果不说了
上面感觉有点繁琐,如果可以把pointcut直接引入就好了
下面直接引入这个切入点就好了
注意这个切入点标签配置的使用位置
另外还需要注意的是这个切入标签的配置,必须放在最前面aspect切面标签前面,反正就是如果配置没问题报错,你就调整一下配置位置
Spring中的环绕通知
再说环绕通知之前,先来看一张图
上面的环绕通知就会有一个明确的切入点,也就是说想要业务代码执行,就必须给环绕通知配一个业务的切入点,下面说一下解决方案
解决:* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。** spring中的环绕通知:* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。*/public Object aroundPringLog(ProceedingJoinPoint pjp){Object rtValue = null;try{Object[] args = pjp.getArgs();//得到方法执行所需的参数System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");return rtValue;}catch (Throwable t){System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");throw new RuntimeException(t);}finally {System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");}}
pring中的环绕通知:
它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
去看一下它的bean.xml配置
下面我们还是来看一下具体的代码
AccountServiceImpl.java
package com.pxx.service.impl;import com.pxx.service.IAccountService;/*** 账户的业务层实现类*/
public class AccountServiceImpl implements IAccountService{@Overridepublic void saveAccount() {System.out.println("执行了保存");
// int i=1/0;}@Overridepublic void updateAccount(int i) {System.out.println("执行了更新"+i);}@Overridepublic int deleteAccount() {System.out.println("执行了删除");return 0;}
}
Logger.java
package com.pxx.utils;import org.aspectj.lang.ProceedingJoinPoint;/*** 用于记录日志的工具类,它里面提供了公共的代码*/
public class Logger {/*** 前置通知*/public void beforePrintLog(){System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");}/*** 后置通知*/public void afterReturningPrintLog(){System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");}/*** 异常通知*/public void afterThrowingPrintLog(){System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");}/*** 最终通知*/public void afterPrintLog(){System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");}/*** 环绕通知* 问题:* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。* 分析:* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。* 解决:* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。** spring中的环绕通知:* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。*/public Object aroundPringLog(ProceedingJoinPoint pjp){Object rtValue = null;try{Object[] args = pjp.getArgs();//得到方法执行所需的参数System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");return rtValue;}catch (Throwable t){System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");throw new RuntimeException(t);}finally {System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");}}
}
AOPTest.java
package com.pxx.test;import com.pxx.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** 测试AOP的配置*/
public class AOPTest {public static void main(String[] args) {//1.获取容器ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");//2.获取对象IAccountService as = (IAccountService)ac.getBean("accountService");//3.执行方法// as.saveAccount();//业务层的每一个方法都会执行通知as.updateAccount(1);}
}
运行结果:
Spring基于注解的AOP配置
我们先来创建一个工程
因为这个又是基于注解配置的,所以约束必须导入带context的内容,还是可以去Spring的核心里面找
直接上这些注解配置
运行结果:
JDBCTemplate的基本使用
先来导一些相关jar包
说一下JdbcTemplate最基本的用法
那么我们就可以用Spring容器的方式来获取对象和设置数据源,来看一下bean.xml里面的代码
贴一段操作代码
package com.itheima.jdbctemplate;import com.itheima.domain.Account;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;import java.sql.ResultSet;
import java.sql.SQLException;/*** JdbcTemplate的CRUD操作*/
public class JdbcTemplateDemo3 {public static void main(String[] args) {//1.获取容器ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");//2.获取对象JdbcTemplate jt = ac.getBean("jdbcTemplate",JdbcTemplate.class);//3.执行操作//保存
// jt.update("insert into account(name,money)values(?,?)","eee",3333f);//更新
// jt.update("update account set name=?,money=? where id=?","test",4567,7);//删除
// jt.update("delete from account where id=?",8);//查询所有
// List<Account> accounts = jt.query("select * from account where money > ?",new AccountRowMapper(),1000f);
// List<Account> accounts = jt.query("select * from account where money > ?",new BeanPropertyRowMapper<Account>(Account.class),1000f);
// for(Account account : accounts){
// System.out.println(account);
// }//查询一个
// List<Account> accounts = jt.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),1);
// System.out.println(accounts.isEmpty()?"没有内容":accounts.get(0));//查询返回一行一列(使用聚合函数,但不加group by子句)Long count = jt.queryForObject("select count(*) from account where money > ?",Long.class,1000f);System.out.println(count);}
}/*** 定义Account的封装策略*/
class AccountRowMapper implements RowMapper<Account>{/*** 把结果集中的数据封装到Account中,然后由spring把每个Account加到集合中* @param rs* @param rowNum* @return* @throws SQLException*/@Overridepublic Account mapRow(ResultSet rs, int rowNum) throws SQLException {Account account = new Account();account.setId(rs.getInt("id"));account.setName(rs.getString("name"));account.setMoney(rs.getFloat("money"));return account;}
}
上面也展示了基本的CURD操作
但是我们具体的业务操作,还是放到dao层的,所以这里来看一个dao层的业务文件
AccountDaoImpl1.java
package com.itheima.dao.impl;import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;import java.util.List;/*** 账户的持久层实现类*/
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {@Overridepublic Account findAccountById(Integer accountId) {List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);return accounts.isEmpty()?null:accounts.get(0);}@Overridepublic Account findAccountByName(String accountName) {List<Account> accounts = super.getJdbcTemplate().query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);if(accounts.isEmpty()){return null;}if(accounts.size()>1){throw new RuntimeException("结果集不唯一");}return accounts.get(0);}@Overridepublic void updateAccount(Account account) {super.getJdbcTemplate().update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());}
}
javaee之spring3相关推荐
- spring3.2入门到大神(备java基础、jsp、servlet,javaee精髓)-任亮-专题视频课程
spring3.2入门到大神(备java基础.jsp.servlet,javaee精髓) 课程介绍 框架介绍,IoC思想.DI依赖注入.Bean的实例方式.Bean种类.Bean作用域 ...
- Cxf + Spring3.0 入门开发WebService
转自原文地址:http://sunny.blog.51cto.com/182601/625540/ 由于公司业务需求, 需要使用WebService技术对外提供服务,以前没有做过类似的项目,在网上搜寻 ...
- Struts2+Spring3.1+Hibernate3.3的整个项目
经过一天的折腾,终于在MyEclipse2013下搭建出一个Struts2+Spring3.1+Hibernate3.3整合的项目,具体过程如下,仅供新手学习,大神勿喷 首先新建Web项目: 直接fi ...
- Struts2.3.4.1+Spring3.2.3+Hibernate4.1.9整合
java教程|Struts2.3.4.1+Spring3.2.3+Hibernate4.1.9整合教程并测试成功一.创建项目二.搭建struts-2.3.4.11.struts2必须的Jar包(放到W ...
- Spring3.2下使用JavaMailSenderImpl类发送邮件
1.JavaMailSenderImpl类 Spring的邮件发送的核心是MailSender接口,在Spring3.0中提供了一个实现类JavaMailSenderImpl,这个类是发送邮件的核心类 ...
- Spring3.1+SpringMVC3.1+JPA2.0
http://www.cnblogs.com/luxh/archive/2012/10/31/2748781.html SpringMVC是越来越火,自己也弄一个Spring+SpringMVC+JP ...
- 使用Maven搭建Struts2+Spring3+Hibernate4的整合开发环境
做了三年多的JavaEE开发了,在平时的JavaEE开发中,为了能够用最快的速度开发项目,一般都会选择使用Struts2,SpringMVC,Spring,Hibernate,MyBatis这些开源框 ...
- 【Struts2+Spring3+Hibernate3】SSH框架整合实现CRUD_1.0
作者: hzboy192@192.com Blog: http://my.csdn.net/peng_hao1988 版本总览:http://blog.csdn.net/peng_hao1988/ar ...
- spring3.2 aop 搭建 (1)
用spring3.2搭建一个aop 1 需要的jar包 2 整个项目图 3 java代码 3.1 Advice.java package aop.advice;public interface Adv ...
最新文章
- PAT甲级1060 Are They Equal:[C++题解]字符串处理、有效数字、代码简洁!!!
- oracle like 条件拼接
- 简单使用JDOM解析XML
- docker可视化管理界面_分析一款Docker容器可视化管理工具Porttainer
- spring boot学习(2) SpringBoot 项目属性配置
- Keil 文本对不上格
- 电力与计算机科学技术,上海电力大学计算机科学与技术专业
- Xv6 traps and system calls
- Misra-Gries 算法
- 数值计算之第三期:直接法解线性方程组
- 电子元件-发光二极管
- 深蓝学院-多传感器融合定位-第4章作业
- IOS11.03越狱
- 第一届腾讯社交广告高校算法大赛经验分享
- mysql实验三报告总结_数据库安全性实验报告的总结(共9篇).docx
- QLabel显示多行文本
- Echarts 图表生成渐变色方法
- Tokyo Cabinet及Tokyo Tyrant tcb tch比较分析
- 全球及中国二手车市场销量渠道规模及发展格局建议报告2021-2027年
- MAC系统IDEA工具栏没有svn图标,svn项目也不显示修改信息