前言
本篇是继上篇MyBatis原理概括延伸的,所以如果有小伙伴还没看上篇博文的话,可以先去看下,也不会浪费大家太多的时间,因为本篇会结合到上篇叙述的相关内容。
好,切入正题,这篇主要讲一个点,就是我们在结合spring去使用mybatis的时候,spring为我们做了什么事。还是老套路,我们只讲过程思路,具体细节还望各位小伙伴找时间去研究,如果我全讲了,你们也都看懂了,那你们最多也就是感到一种获得感,而不是成就感,获得感是会随着时间的推移而慢慢减少的,所以我这里主要提供给大家一个思路,然后大家可以顺着这条思路慢慢摸索下去,从而获得成就感!
使用spring-mybatis
1.spring-mybatis是什么
MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。 使用这个类库中的类, Spring 将会加载必要的 MyBatis 工厂类和 session 类。 这个类库也提供一个简单的方式来注入 MyBatis 数据映射器和 SqlSession 到业务层的 bean 中。 而且它也会处理事务, 翻译 MyBatis 的异常到 Spring 的 DataAccessException 异常(数据访问异常,译者注)中。最终,它并 不会依赖于 MyBatis,Spring 或 MyBatis-Spring 来构建应用程序代码。(这是官网解释)
2.基于XML配置和注解形式使用
a.基于XML配置
一般情况下,我们使用xml的形式引入mybatis,一般的配置如下:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="${maxIdle}"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="${minIdle}"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="${maxWait}"></property>
</bean>
<!-- spring和MyBatis的完美结合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:com/javen/mapping/*.xml"></property>
</bean>
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.javen.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
如上配置所示,我们一般需要申明dataSource、sqlSessionFactory以及MapperScannerConfigurer。如何我们还有其他mybatis的配置,比如plugin、typehandler等,我们可以另外申明一个mybaits-config.xml文件,在sqlSessionFactory配置中引入即可。下面对各部分作用总结下。
dataSource:申明一个数据源;
sqlSessionFactory:申明一个sqlSession的工厂;
MapperScannerConfigurer:让spring自动扫描我们持久层的接口从而自动构建代理类。
b.基于注解形式
注解形式的话相当于将上述的xml配置一一对应成注解的形式
@Configuration
@MapperScan(value="org.fhp.springmybatis.dao")
public class DaoConfig {
@Value("${jdbc.driverClass}")
private String driverClass;
@Value("${jdbc.user}")
private String user;
@Value("${jdbc.password}")
private String password;
@Value("${jdbc.jdbcUrl}")
private String jdbcUrl;
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUsername(user);
dataSource.setPassword(password);
dataSource.setUrl(jdbcUrl);
return dataSource;
}
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
return sessionFactory.getObject();
}
}
很明显,一样需要一个dataSource,SqlSessionFactory以及一个@MapperScan的注解。这个注解的作用跟上述的
MapperScannerConfigurer的作用是一样的。
3.spring和mybatis无缝整合的机制
a.BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar的认识
在讲mybatis如何无缝整合进spring之前,我们先认识下BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar这两个接口的作用。
我们先看下这两个接口是什么样的。
//BeanDefinitionRegistryPostProcessor接口
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}
//ImportBeanDefinitionRegistrar接口
public interface ImportBeanDefinitionRegistrar {
void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2);
}
对于这两个接口我们先看官方文档给我们的解释。
以下是BeanDefinitionRegistryPostProcessor的解释:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor
Extension to the standard BeanFactoryPostProcessor SPI, allowing for the registration of further bean definitions before regular BeanFactoryPostProcessor detection kicks in. In particular, BeanDefinitionRegistryPostProcessor may register further bean definitions which in turn define BeanFactoryPostProcessor instances.
意思大概就是我们可以扩展spring对于bean definitions的定义。也就是说可以让我们实现自定义的注册bean定义的逻辑。
再来看下ImportBeanDefinitionRegistrar的解释:
public interface ImportBeanDefinitionRegistrar Interface to be implemented by types that register additional bean definitions when processing @Configuration classes. Useful when operating at the bean definition level (as opposed to @Bean method/instance level) is desired or necessary.
Along with @Configuration and ImportSelector, classes of this type may be provided to the @Import annotation (or may also be returned from an ImportSelector).
通俗解释来讲就是在@Configuration上使用@Import时可以自定义beanDefinition,或者作为ImportSelector接口的返回值(有兴趣的小伙伴可以自行研究)。
所以总结下就是如果我想扩展beanDefinition那么我可以继承这两个接口实现。下面我们就从mybatis配置方式入手讲讲spring和mybatis是如何无缝整合的。
b.基于XML配置mybatis是如何整合进spring的
首先,容器启动的时候,我们在xml配置中的SqlSessionFactoryBean会被初始化,所以我们先看下SqlSessionFactoryBean是在初始化的时候作了哪些工作。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
private Resource configLocation;
private Configuration configuration;
private Resource[] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
private String environment = SqlSessionFactoryBean.class.getSimpleName();
private boolean failFast;
private Interceptor[] plugins;
private TypeHandler<?>[] typeHandlers;
private String typeHandlersPackage;
private Class<?>[] typeAliases;
private String typeAliasesPackage;
private Class<?> typeAliasesSuperType;
private DatabaseIdProvider databaseIdProvider;
private Class<? extends VFS> vfs;
private Cache cache;
private ObjectFactory objectFactory;
private ObjectWrapperFactory objectWrapperFactory;
public SqlSessionFactoryBean() {
}
...
}
我们可以看到这个类实现了FactoryBean、InitializingBean和ApplicationListener接口,对应的接口在bean初始化的时候为执行些特定的方法(如果不清楚的小伙伴请自行百度,这里不作过多叙述)。现在我们来看看都有哪些方法会被执行,这些方法又作了哪些工作。
//FactoryBean
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
this.afterPropertiesSet();
}
return this.sqlSessionFactory;
}
//InitializingBean
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.dataSource, "Property 'dataSource' is required");
Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = this.buildSqlSessionFactory();
}
//ApplicationListener
public void onApplicationEvent(ApplicationEvent event) {
if (this.failFast && event instanceof ContextRefreshedEvent) {
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
通过观察代码我们可以知道前面两个都是在做同一件事情,那就是在构建sqlSessionFactory,在构建sqlSessionFactory时mybatis会去解析配置文件,构建configuation。后面的onApplicationEvent主要是监听应用事件时做的一些事情(不详讲,有兴趣的同学可以自己去了解下)。
其次,我们回忆下我们在xml配置中还配置了MapperScannerConfigurer,或者也可以配置多个的MapperFactoryBean,道理都是一样的,只是MapperScannerConfigurer帮我们封装了这一个过程,可以实现自动扫描指定包下的mapper接口构建MapperFactoryBean。
问题1:为什么我们从spring容器中能直接获取对应mapper接口的实现类?而不用使用sqlSession去getMapper呢?
答案其实在上面就已经为大家解答了,就是MapperFactoryBean。我们先看看这个类。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
}
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
...
}
这个类继承了SqlSessionDaoSupport,实现了FactoryBean。
我们先讲讲SqlSessionDaoSupport这个类
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
public SqlSessionDaoSupport() {
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
public SqlSession getSqlSession() {
return this.sqlSession;
}
protected void checkDaoConfig() {
Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
可以看到这个类继承了DaoSupport,我们再来看下这个类。
public abstract class DaoSupport implements InitializingBean {
protected final Log logger = LogFactory.getLog(this.getClass());
public DaoSupport() {
}
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
this.checkDaoConfig();
try {
this.initDao();
} catch (Exception var2) {
throw new BeanInitializationException("Initialization of DAO failed", var2);
}
}
protected abstract void checkDaoConfig() throws IllegalArgumentException;
protected void initDao() throws Exception {
}
}
可以看到实现了InitializingBean接口,所以在类初始化时为执行afterPropertiesSet方法,我们看到afterPropertiesSet方法里面有checkDaoConfig方法和initDao方法,其中initDao是模板方法,提供子类自行实现相关dao初始化的操作,我们看下checkDaoConfig方法作了什么事。
//MapperFactoryBean
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
这个方法具体的实现是在MapperFactoryBean类里面的,主要作用就是对验证mapperInterface是否存在configuration对象里面。
然后我们再来看下MapperFactoryBean实现了FactoryBean的目的是什么。我们都知道FactoryBean有一个方法是getObject,这个方法的作用就是在spring容器初始化bean时,如果判断这个类是否继承自FactoryBean,那么在获取真正的bean实例时会调用getObject,将getObject方法返回的值注册到spring容器中。在明白了这些知识点之后,我们看下MapperFactoryBean的getObject方法是如何实现的。
//MapperFactoryBean
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
看到这里是否就已经明白为什么在结合spring时我们不需要使用sqlSession对象去获取我们的mapper实现类了吧。因为spring帮我们作了封装!
之后的操作可以结合上面博文去看mybatis如何获取到对应的Mapper对象的了。附上上篇博文地址:MyBatis原理概括。
接下来我们看下mybatis是如何结合spring构建MapperFactoryBean的beanDefinition的。这里我们需要看看MapperScannerConfigurer这个类,这个类的目的就是扫描我们指定的dao层(持久层)对应的包(package),构建相应的beanDefinition提供给spring容器去实例化我们的mapper接口对象。
//MapperScannerConfigurer
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
private String basePackage;
private boolean addToConfig = true;
private SqlSessionFactory sqlSessionFactory;
private SqlSessionTemplate sqlSessionTemplate;
private String sqlSessionFactoryBeanName;
private String sqlSessionTemplateBeanName;
private Class<? extends Annotation> annotationClass;
private Class<?> markerInterface;
private ApplicationContext applicationContext;
private String beanName;
private boolean processPropertyPlaceHolders;
private BeanNameGenerator nameGenerator;
public MapperScannerConfigurer() {
}
...
}
通过代码,我们可以看到这个类实现了BeanDefinitionRegistryPostProcessor这个接口,通过前面对BeanDefinitionRegistryPostProcessor的讲解,我们去看看MapperScannerConfigurer中的postProcessBeanDefinitionRegistry方法的实现。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
this.processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
}
可以看这里就是在构建ClassPathMapperScanner对象,然后调用scan方法扫描。接下来我们继续看这个扫描的操作,因为这个类继承了ClassPathBeanDefinitionScanner,调用的scan方法是在ClassPathBeanDefinitionScanner里申明的。
//ClassPathBeanDefinitionScanner
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
this.doScan(basePackages);
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return this.registry.getBeanDefinitionCount() - beanCountAtScanStart;
}
这里我们需要注意doScan这个方法,这个方法在ClassPathMapperScanner中重写了。
//ClassPathMapperScanner
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
这里调用了父类的doScan得到beanDefinitions的集合。这里的父类的doScan方法是spring提供的包扫描操作,这里不过多叙述,感兴趣的小伙伴可以自行研究。我们还注意到在得到beanDefinitions集合后,这里还调用了processBeanDefinitions方法,这里是对beanDefinition做了一些特殊的处理以满足mybaits的需求。我们先来看下这个方法。
//ClassPathMapperScanner#doScan
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
Iterator var3 = beanDefinitions.iterator();
while(var3.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(2);
}
}
}
这里我们注意到有这么一行代码:definition.setBeanClass(this.mapperFactoryBean.getClass()),看到这里我们就可以知道为什么spring在加载初始化我们的mapper接口对象会初始化成MapperFactoryBean对象了。
好了,到这里我们也就明白了spring是如何帮我们加载注册我们的mapper接口对应的实现类了。对于代码里涉及到的其他细节,这里暂时不作过多讲解,还是老套路,只讲解总体思路。
c.基于注解配置mybatis是如何整合进spring的
基于注解形式的配置其实就是将xml配置对应到注解中来,本质上的流程还是一样的。所以这里我就简单讲讲。我们先看看MapperScannerRegistrar这个类,因为这个类是spring构建MapperFactoryBean的核心类。
//MapperScannerRegistrar
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
public MapperScannerRegistrar() {
}
...
}
这里我们注意到MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,在前面的叙述中我们已经知道了实现ImportBeanDefinitionRegistrar接口的作用是什么了,所以我们直接看看这里具体做了什么操作。
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}
Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}
Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator((BeanNameGenerator)BeanUtils.instantiateClass(generatorClass));
}
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean((MapperFactoryBean)BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
List<String> basePackages = new ArrayList();
String[] var10 = annoAttrs.getStringArray("value");
int var11 = var10.length;
int var12;
String pkg;
for(var12 = 0; var12 < var11; ++var12) {
pkg = var10[var12];
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
var10 = annoAttrs.getStringArray("basePackages");
var11 = var10.length;
for(var12 = 0; var12 < var11; ++var12) {
pkg = var10[var12];
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
Class[] var14 = annoAttrs.getClassArray("basePackageClasses");
var11 = var14.length;
for(var12 = 0; var12 < var11; ++var12) {
Class<?> clazz = var14[var12];
basePackages.add(ClassUtils.getPackageName(clazz));
}
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(basePackages));
}
通过观察我们看到最后还是调用了ClassPathMapperScanner的doScan去扫描指定包下的mapper接口(持久层),然后构建对应的beanDefinition类。前面我们知道是通过MapperScan这个注解去指定包的,然后我们也可以看到,在这个方法一开始就取出这个注解的值,然后进行接下来的操作的。
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
之后的过程其实跟xml形式配置的一样了。
后序
好啦,这篇没想啰理八嗦说了那么多,可能有好多小伙伴看到最后也是懵逼状态,这里有个建议,打开IDE,边看边对着代码跟踪,如果哪里觉得不对,可以直接debug。
这里给大家提个看源码的建议,就是猜想+验证。先猜想自己的想法,然后通过查找相关问题或者debug代码去验证自己的思路。
好啦,到这里为止,mybatis和spring-mybatis的基本原理都跟大家说了一遍,不知道小伙伴们有没有收获呢,下一篇,我会带大家手写一遍mybatis,是纯手写而且还能跑起来的那种哦!
注:本人不才,以上如有错误的地方或者不规范的叙述还望各位小伙伴批评指点。