我叫DMZ

我叫DMZ 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

我叫DMZ 发布了文章 · 2020-07-22

Spring中异步注解@Async的使用、原理及使用时可能导致的问题

前言

其实最近都在研究事务相关的内容,之所以写这么一篇文章是因为前面写了一篇关于循环依赖的文章:

面试必杀技,讲一讲Spring中的循环依赖

然后,很多同学碰到了下面这个问题,添加了Spring提供的一个异步注解@Async循环依赖无法被解决了,下面是一些读者的留言跟群里同学碰到的问题:

image-20200719200303749

image-20200719200244719

本着讲一个知识点就要讲明白、讲透彻的原则,我决定单独写一篇这样的文章对@Async这个注解做一下详细的介绍,这个注解带来的问题远远不止循环依赖这么简单,如果对它不够熟悉的话建议慎用。

文章要点

image-20200719201511174

@Async的基本使用

这个注解的作用在于可以让被标注的方法异步执行,但是有两个前提条件

  1. 配置类上添加@EnableAsync注解
  2. 需要异步执行的方法的所在类由Spring管理
  3. 需要异步执行的方法上添加了@Async注解

我们通过一个Demo体会下这个注解的作用吧

第一步,配置类上开启异步:

@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
public class Config {

}

第二步,

@Component  // 这个类本身要被Spring管理
public class DmzAsyncService {
    
    @Async  // 添加注解表示这个方法要异步执行
    public void testAsync(){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("testAsync invoked");
    }
}

第三步,测试异步执行

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
        DmzAsyncService bean = ac.getBean(DmzAsyncService.class);
        bean.testAsync();
        System.out.println("main函数执行完成");
    }
}
// 程序执行结果如下:
// main函数执行完成
// testAsync invoked

通过上面的例子我们可以发现,DmzAsyncService中的testAsync方法是异步执行的,那么这背后的原理是什么呢?我们接着分析

原理分析

我们在分析某一个技术的时候,最重要的事情是,一定一定要找到代码的入口,像Spring这种都很明显,入口必定是在@EnableAsync这个注解上面,我们来看看这个注解干了啥事(本文基于5.2.x版本)

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 这里是重点,导入了一个ImportSelector
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
    
    // 这个配置可以让程序员配置需要被检查的注解,默认情况下检查的就是@Async注解
    Class<? extends Annotation> annotation() default Annotation.class;
    
    // 默认使用jdk代理
    boolean proxyTargetClass() default false;
    
    // 默认使用Spring AOP
    AdviceMode mode() default AdviceMode.PROXY;
    
    // 在后续分析我们会发现,这个注解实际往容器中添加了一个
    // AsyncAnnotationBeanPostProcessor,这个后置处理器实现了Ordered接口
    // 这个配置主要代表了AsyncAnnotationBeanPostProcessor执行的顺序
    int order() default Ordered.LOWEST_PRECEDENCE;
}

上面这个注解做的最重要的事情就是导入了一个AsyncConfigurationSelector,这个类的源码如下:

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

    private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
            "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";

    @Override
    @Nullable
    public String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
                // 默认会使用SpringAOP进行代理
            case PROXY:
                return new String[] {ProxyAsyncConfiguration.class.getName()};
            case ASPECTJ:
                return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
            default:
                return null;
        }
    }

}

这个类的作用是像容器中注册了一个ProxyAsyncConfiguration,这个类的继承关系如下:

image-20200719220316319

我们先看下它的父类AbstractAsyncConfiguration,其源码如下:

@Configuration
public abstract class AbstractAsyncConfiguration implements ImportAware {
    
    @Nullable
    protected AnnotationAttributes enableAsync;

    @Nullable
    protected Supplier<Executor> executor;

    @Nullable
    protected Supplier<AsyncUncaughtExceptionHandler> exceptionHandler;
    
    // 这里主要就是检查将其导入的类上是否有EnableAsync注解
    // 如果没有的话就报错
    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        this.enableAsync = AnnotationAttributes.fromMap(
                importMetadata.getAnnotationAttributes(EnableAsync.class.getName(), false));
        if (this.enableAsync == null) {
            throw new IllegalArgumentException(
                    "@EnableAsync is not present on importing class " + importMetadata.getClassName());
        }
    }
    
    // 将容器中配置的AsyncConfigurer注入
    // 异步执行嘛,所以我们可以配置使用的线程池
    // 另外也可以配置异常处理器
    @Autowired(required = false)
    void setConfigurers(Collection<AsyncConfigurer> configurers) {
        if (CollectionUtils.isEmpty(configurers)) {
            return;
        }
        if (configurers.size() > 1) {
            throw new IllegalStateException("Only one AsyncConfigurer may exist");
        }
        AsyncConfigurer configurer = configurers.iterator().next();
        this.executor = configurer::getAsyncExecutor;
        this.exceptionHandler = configurer::getAsyncUncaughtExceptionHandler;
    }

}

再来看看ProxyAsyncConfiguration这个类的源码

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {

    @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
        AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
        // 将通过AsyncConfigurer配置好的线程池跟异常处理器设置到这个后置处理器中
        bpp.configure(this.executor, this.exceptionHandler);
        Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
        if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
            bpp.setAsyncAnnotationType(customAsyncAnnotation);
        }
        bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
        bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
        return bpp;
    }

}

这个类本身是一个配置类,它的作用是向容器中添加一个AsyncAnnotationBeanPostProcessor。到这一步我们基本上就可以明白了,@Async注解的就是通过AsyncAnnotationBeanPostProcessor这个后置处理器生成一个代理对象来实现异步的,接下来我们就具体看看AsyncAnnotationBeanPostProcessor是如何生成代理对象的,我们主要关注一下几点即可:

  1. 是在生命周期的哪一步完成的代理?
  2. 切点的逻辑是怎么样的?它会对什么样的类进行拦截?
  3. 通知的逻辑是怎么样的?是如何实现异步的?

基于上面几个问题,我们进行逐一分析

是在生命周期的哪一步完成的代理?

我们抓住重点,AsyncAnnotationBeanPostProcessor是一个后置处理器器,按照我们对Spring的了解,大概率是在这个后置处理器的postProcessAfterInitialization方法中完成了代理,直接定位到这个方法,这个方法位于父类AbstractAdvisingBeanPostProcessor中,具体代码如下:

public Object postProcessAfterInitialization(Object bean, String beanName) {
    // 没有通知,或者是AOP的基础设施类,那么不进行代理
    if (this.advisor == null || bean instanceof AopInfrastructureBean) {
        return bean;
    }
    
    // 对已经被代理的类,不再生成代理,只是将通知添加到代理类的逻辑中
    // 这里通过beforeExistingAdvisors决定是将通知添加到所有通知之前还是添加到所有通知之后
    // 在使用@Async注解的时候,beforeExistingAdvisors被设置成了true
    // 意味着整个方法及其拦截逻辑都会异步执行
    if (bean instanceof Advised) {
        Advised advised = (Advised) bean;
        if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
            if (this.beforeExistingAdvisors) {
                advised.addAdvisor(0, this.advisor);
            }
            else {
                advised.addAdvisor(this.advisor);
            }
            return bean;
        }
    }
    
    // 判断需要对哪些Bean进行来代理
    if (isEligible(bean, beanName)) {
        ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
        if (!proxyFactory.isProxyTargetClass()) {
            evaluateProxyInterfaces(bean.getClass(), proxyFactory);
        }
        proxyFactory.addAdvisor(this.advisor);
        customizeProxyFactory(proxyFactory);
        return proxyFactory.getProxy(getProxyClassLoader());
    }
    return bean;
}

果不其然,确实是在这个方法中完成的代理。接着我们就要思考,切点的过滤规则是什么呢?

切点的逻辑是怎么样的?

其实也不难猜到肯定就是类上添加了@Async注解或者类中含有被@Async注解修饰的方法。基于此,我们看看这个isEligible这个方法的实现逻辑,这个方位位于AbstractBeanFactoryAwareAdvisingPostProcessor中,也是AsyncAnnotationBeanPostProcessor的父类,对应代码如下:

// AbstractBeanFactoryAwareAdvisingPostProcessor的isEligible方法
// 调用了父类
protected boolean isEligible(Object bean, String beanName) {
    return (!AutoProxyUtils.isOriginalInstance(beanName, bean.getClass()) &&
            super.isEligible(bean, beanName));
}

protected boolean isEligible(Object bean, String beanName) {
    return isEligible(bean.getClass());
}

protected boolean isEligible(Class<?> targetClass) {
    Boolean eligible = this.eligibleBeans.get(targetClass);
    if (eligible != null) {
        return eligible;
    }
    if (this.advisor == null) {
        return false;
    }
    // 这里完成的判断
    eligible = AopUtils.canApply(this.advisor, targetClass);
    this.eligibleBeans.put(targetClass, eligible);
    return eligible;
}

实际上最后就是根据advisor来确定是否要进行代理,在Spring中AOP相关的API及源码解析,原来AOP是这样子的这篇文章中我们提到过,advisor实际就是一个绑定了切点的通知,那么AsyncAnnotationBeanPostProcessor这个advisor是什么时候被初始化的呢?我们直接定位到AsyncAnnotationBeanPostProcessorsetBeanFactory方法,其源码如下:

public void setBeanFactory(BeanFactory beanFactory) {
    super.setBeanFactory(beanFactory);
    
    // 在这里new了一个AsyncAnnotationAdvisor
    AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
    if (this.asyncAnnotationType != null) {
        advisor.setAsyncAnnotationType(this.asyncAnnotationType);
    }
    advisor.setBeanFactory(beanFactory);
    // 完成了初始化
    this.advisor = advisor;
}

我们来看看AsyncAnnotationAdvisor中的切点匹配规程是怎么样的,直接定位到这个类的buildPointcut方法中,其源码如下:

protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {
    ComposablePointcut result = null;
    for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
        // 就是根据这两个匹配器进行匹配的
        Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
        Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
        if (result == null) {
            result = new ComposablePointcut(cpc);
        }
        else {
            result.union(cpc);
        }
        result = result.union(mpc);
    }
    return (result != null ? result : Pointcut.TRUE);
}

代码很简单,就是根据cpc跟mpc两个匹配器来进行匹配的,第一个是检查类上是否有@Async注解,第二个是检查方法是是否有@Async注解。

那么,到现在为止,我们已经知道了它在何时创建代理,会为什么对象创建代理,最后我们还需要解决一个问题,代理的逻辑是怎么样的,异步到底是如何实现的?

通知的逻辑是怎么样的?是如何实现异步的?

前面也提到了advisor是一个绑定了切点的通知,前面分析了它的切点,那么现在我们就来看看它的通知逻辑,直接定位到AsyncAnnotationAdvisor中的buildAdvice方法,源码如下:

protected Advice buildAdvice(
    @Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {

    AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
    interceptor.configure(executor, exceptionHandler);
    return interceptor;
}

简单吧,加了一个拦截器而已,对于interceptor类型的对象,我们关注它的核心方法invoke就行了,代码如下:

public Object invoke(final MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
    final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
    
    // 异步执行嘛,先获取到一个线程池
    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
    if (executor == null) {
        throw new IllegalStateException(
            "No executor specified and no default executor set on AsyncExecutionInterceptor either");
    }
    
    // 然后将这个方法封装成一个 Callable对象传入到线程池中执行
    Callable<Object> task = () -> {
        try {
            Object result = invocation.proceed();
            if (result instanceof Future) {
                return ((Future<?>) result).get();
            }
        }
        catch (ExecutionException ex) {
            handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
        }
        catch (Throwable ex) {
            handleError(ex, userDeclaredMethod, invocation.getArguments());
        }
        return null;
    };
    // 将任务提交到线程池
    return doSubmit(task, executor, invocation.getMethod().getReturnType());
}

导致的问题及解决方案

问题1:循环依赖报错

就像在这张图里这个读者问的问题,image-20200719200303749

分为两点回答:

第一:循环依赖为什么不能被解决?

这个问题其实很简单,在《面试必杀技,讲一讲Spring中的循环依赖》这篇文章中我从两个方面分析了循环依赖的处理流程

  1. 简单对象间的循环依赖处理
  2. AOP对象间的循环依赖处理

按照这种思路,@Async注解导致的循环依赖应该属于AOP对象间的循环依赖,也应该能被处理。但是,重点来了,解决AOP对象间循环依赖的核心方法是三级缓存,如下:

image-20200706105535307

在三级缓存缓存了一个工厂对象,这个工厂对象会调用getEarlyBeanReference方法来获取一个早期的代理对象的引用,其源码如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
   Object exposedObject = bean;
   if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
          // 看到这个判断了吗,通过@EnableAsync导入的后置处理器
          // AsyncAnnotationBeanPostProcessor根本就不是一个SmartInstantiationAwareBeanPostProcessor
          // 这就意味着即使我们通过AsyncAnnotationBeanPostProcessor创建了一个代理对象
          // 但是早期暴露出去的用于给别的Bean进行注入的那个对象还是原始对象
         if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
         }
      }
   }
   return exposedObject;
}

看完上面的代码循环依赖的问题就很明显了,因为早期暴露的对象跟最终放入容器中的对象不是同一个,所以报错了。报错的具体位置我在你知道Spring是怎么将AOP应用到Bean的生命周期中的吗? 文章末尾已经分析过了,本文不再赘述

image-20200720152830307

解决方案

就以上面读者给出的Demo为例,只需要在为B注入A时添加一个@Lazy注解即可

@Component
public class B implements BService {
    
    @Autowired
    @Lazy
    private A a;

    public void doSomething() {
    }
}

这个注解的作用在于,当为B注入A时,会为A生成一个代理对象注入到B中,当真正调用代理对象的方法时,底层会调用getBean(a)去创建A对象,然后调用方法,这个注解的处理时机是在org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency方法中,处理这个注解的代码位于org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver#buildLazyResolutionProxy,这些代码其实都在我之前的文章中分析过了

Spring杂谈 | Spring中的AutowireCandidateResolver

谈谈Spring中的对象跟Bean,你知道Spring怎么创建对象的吗?

所以本文不再做详细分析

问题2:默认线程池不会复用线程

我觉得这是这个注解最坑的地方,没有之一!我们来看看它默认使用的线程池是哪个,在前文的源码分析中,我们可以看到决定要使用线程池的方法是org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor。其源码如下:

protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
    AsyncTaskExecutor executor = this.executors.get(method);
    if (executor == null) {
        Executor targetExecutor;
        // 可以在@Async注解中配置线程池的名字
        String qualifier = getExecutorQualifier(method);
        if (StringUtils.hasLength(qualifier)) {
            targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
        }
        else {
            // 获取默认的线程池
            targetExecutor = this.defaultExecutor.get();
        }
        if (targetExecutor == null) {
            return null;
        }
        executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
                    (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
        this.executors.put(method, executor);
    }
    return executor;
}

最终会调用到org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor这个方法中

protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
   Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
   return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}

可以看到,它默认使用的线程池是SimpleAsyncTaskExecutor。我们不看这个类的源码,只看它上面的文档注释,如下:

image-20200720160047340

主要说了三点

  1. 为每个任务新起一个线程
  2. 默认线程数不做限制
  3. 不复用线程

就这三点,你还敢用吗?只要你的任务耗时长一点,说不定服务器就给你来个OOM

解决方案

最好的办法就是使用自定义的线程池,主要有这么几种配置方法

  1. 在之前的源码分析中,我们可以知道,可以通过AsyncConfigurer来配置使用的线程池

如下:

public class DmzAsyncConfigurer implements AsyncConfigurer {
   @Override
   public Executor getAsyncExecutor() {
      // 创建自定义的线程池
   }
}
  1. 直接在@Async注解中配置要使用的线程池的名称

如下:

public class A implements AService {
    
    private B b;

    @Autowired
    public void setB(B b) {
        System.out.println(b);
        this.b = b;
    }

    @Async("dmzExecutor")
    public void doSomething() {
    }
}
@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
@Aspect
public class Config {
    @Bean("dmzExecutor")
    public Executor executor(){
        // 创建自定义的线程池
        return executor;
    }
}

总结

本文主要介绍了Spring中异步注解的使用、原理及可能碰到的问题,针对每个问题文中也给出了方案。希望通过这篇文章能帮助你彻底掌握@Async注解的使用,知其然并知其所以然!

文章有帮到你的话,记得点个赞哈~
如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

查看原文

赞 1 收藏 1 评论 0

我叫DMZ 发布了文章 · 2020-07-21

Spring事务源码分析专题(一)JdbcTemplate使用及源码分析

Spring中的数据访问,JdbcTemplate使用及源码分析

前言

本系列文章为事务专栏分析文章,整个事务分析专题将按下面这张图完成

image-20200718220712800

image-20200718220712800

对源码分析前,我希望先介绍一下Spring中数据访问的相关内容,然后层层递进到事物的源码分析,主要分为两个部分

  1. JdbcTemplate使用及源码分析
  2. Mybatis的基本使用及Spring对Mybatis的整合

本文将要介绍的是第一点。

JdbcTemplate使用示例

public class DmzService {

 private JdbcTemplate jdbcTemplate;

 public void setDataSource(DataSource dataSource) {
  jdbcTemplate = new JdbcTemplate(dataSource);
 }

 /**
  * 查询
  * @param id 根据id查询
  * @return 对应idd的user对象
  */
 public User getUserById(int id) {
  return jdbcTemplate
    .queryForObject("select * from `user` where id  =  ?", new RowMapper<User>() {
     @Override
     public User mapRow(ResultSet rs, int rowNum) throws SQLException {
      User user = new User();
      user.setId(rs.getInt("id"));
      user.setAge(rs.getInt("age"));
      user.setName(rs.getString("name"));
      return user;
     }
    }, id);
 }

 public int saveUser(User user){
  return jdbcTemplate.update("insert into user values(?,?,?)",
    new Object[]{user.getId(),user.getName(),user.getAge()});
 }
}
public class Main {
 public static void main(String[] args) {
  ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("tx.xml");
  DmzService dmzService = cc.getBean(DmzService.class);
  User userById = dmzService.getUserById(1);
  System.out.println("查询的数据为:" + userById);
  userById.setId(userById.getId() + 1);
  int i = dmzService.saveUser(userById);
  System.out.println("插入了" + i + "条数据");
 }
}

数据库中目前只有一条数据:

image-20200708153438245

image-20200708153438245

配置文件如下:

<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

 <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
  <property name="password" value="123"/>
  <property name="username" value="root"/>
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url"
      value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/>

 </bean>

 <bean id="dmzService" class="com.dmz.spring.tx.service.DmzService">
  <property name="dataSource" ref="dataSource"/>
  </bean>
</beans>

程序允许结果:

查询的数据为:User{id=1, name='dmz', age=18}
插入了1条数据

image-20200708153656393

image-20200708153656393

运行后数据库中确实插入了一条数据

对于JdbcTemplate的简单使用,建议大家还是要有一定熟悉,虽然我现在在项目中不会直接使用JdbcTemplate的API。本文关于使用不做过多介绍,主要目的是分析它底层的源码

JdbcTemplate源码分析

我们直接以其queryForObject方法为入口,对应源码如下:

queryForObject方法分析

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException {
   
    // 核心在这个query方法中
    List<T> results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1));
   
    // 这个方法很简单,就是返回结果集中的数据
    // 如果少于1条或者多余1条都报错
    return DataAccessUtils.nullableSingleResult(results);
}

query方法分析

// 第一步,对传入的参数进行封装,将参数封装成ArgumentPreparedStatementSetter
public <T> T query(String sql, @Nullable Object[] args, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(sql, newArgPreparedStatementSetter(args), rse);
}

// 第二步:对sql语句进行封装,将sql语句封装成SimplePreparedStatementCreator
public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(new SimplePreparedStatementCreator(sql), pss, rse);
}


public <T> T query(
    PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
    throws DataAccessException {
 // query方法在完成对参数及sql语句的封装后,直接调用了execute方法
    // execute方法是jdbcTemplate的基本API,不管是查询、更新还是保存
    // 最终都会进入到这个方法中
    return execute(psc, new PreparedStatementCallback<T>() {
        @Override
        @Nullable
        public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
            ResultSet rs = null;
            try {
                if (pss != null) {
                    pss.setValues(ps);
                }
                rs = ps.executeQuery();
                return rse.extractData(rs);
            }
            finally {
                JdbcUtils.closeResultSet(rs);
                if (pss instanceof ParameterDisposer) {
                    ((ParameterDisposer) pss).cleanupParameters();
                }
            }
        }
    });
}

execute方法分析

// execute方法封装了一次数据库访问的基本操作
// 例如:获取连接,释放连接等
// 其定制化操作是通过传入的PreparedStatementCallback参数来实现的
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
    throws DataAccessException {
 // 1.获取数据库连接
    Connection con = DataSourceUtils.getConnection(obtainDataSource());
    PreparedStatement ps = null;
    try {
        // 2.获取一个PreparedStatement,并应用用户设定的参数
        ps = psc.createPreparedStatement(con);
        applyStatementSettings(ps);
        // 3.执行sql并返回结果
        T result = action.doInPreparedStatement(ps);
        // 4.处理警告
        handleWarnings(ps);
        return result;
    }
    catch (SQLException ex) {
       // 出现异常的话,需要关闭数据库连接
        if (psc instanceof ParameterDisposer) {
            ((ParameterDisposer) psc).cleanupParameters();
        }
        String sql = getSql(psc);
        psc = null;
        JdbcUtils.closeStatement(ps);
        ps = null;
        DataSourceUtils.releaseConnection(con, getDataSource());
        con = null;
        throw translateException("PreparedStatementCallback", sql, ex);
    }
    finally {
        // 关闭资源
        if (psc instanceof ParameterDisposer) {
            ((ParameterDisposer) psc).cleanupParameters();
        }
        JdbcUtils.closeStatement(ps);
        DataSourceUtils.releaseConnection(con, getDataSource());
    }
}

1、获取数据库连接

对应源码如下:

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
    // 这里省略了异常处理
    // 直接调用了doGetConnection方法
    return doGetConnection(dataSource); 
  
}

doGetConnection方法是最终获取连接的方法

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    Assert.notNull(dataSource, "No DataSource specified");

    // 如果使用了事务管理器来对事务进行管理(申明式事务跟编程式事务都依赖于事务管理器)
    // 那么在开启事务时,Spring会提前绑定一个数据库连接到当前线程中
    // 这里做的就是从当前线程中获取对应的连接池中的连接
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
        // 记录当前这个连接被使用的次数,每次调用+1
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            logger.debug("Fetching resumed JDBC Connection from DataSource");
            conHolder.setConnection(fetchConnection(dataSource));
        }
        return conHolder.getConnection();
    }
    Connection con = fetchConnection(dataSource);

    // 如果开启了一个空事务(例如事务的传播级别设置为SUPPORTS时,就会开启一个空事务)
    // 会激活同步,那么在这里需要将连接绑定到当前线程
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        try {
            ConnectionHolder holderToUse = conHolder;
            if (holderToUse == null) {
                holderToUse = new ConnectionHolder(con);
            }
            else {
                holderToUse.setConnection(con);
            }
            // 当前连接被使用的次数+1(能进入到这个方法,说明这个连接是刚刚从连接池中获取到)
            // 当释放资源时,只有被使用的次数归为0时才放回到连接池中
            holderToUse.requested();
            TransactionSynchronizationManager.registerSynchronization(
                new ConnectionSynchronization(holderToUse, dataSource));
            holderToUse.setSynchronizedWithTransaction(true);
            if (holderToUse != conHolder) {
                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
            }
        }
        catch (RuntimeException ex) {
   // 出现异常时释放连接,如果开启了事务,不会真正调用close方法关闭连接
            // 而是把当前连接的使用数-1
            releaseConnection(con, dataSource);
            throw ex;
        }
    }

    return con;
}

2、应用用户设定的参数

protected void applyStatementSettings(Statement stmt) throws SQLException {
    int fetchSize = getFetchSize();
    if (fetchSize != -1) {
        stmt.setFetchSize(fetchSize);
    }
    int maxRows = getMaxRows();
    if (maxRows != -1) {
        stmt.setMaxRows(maxRows);
    }
    DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
}

从上面代码可以看出,主要设立了两个参数

  1. fetchSize:该参数的设计目的主要是为了减少网络交互,当访问ResultSet的时候,如果它每次只从服务器读取一条数据,则会产生大量的开销,setFetchSize的含义在于,当调用rs.next时,它可以直接从内存中获取而不需要网络交互,提高了效率。这个设置可能会被某些JDBC驱动忽略,而且设置过大会造成内存上升
  2. setMaxRows,是将此Statement生成的所有ResultSet的最大返回行数限定为指定数,作用类似于limit。

3、执行Sql

没啥好说的,底层其实就是调用了jdbc的一系列API

4、处理警告

也没啥好说的,处理Statement中的警告信息

protected void handleWarnings(Statement stmt) throws SQLException {
    if (isIgnoreWarnings()) {
        if (logger.isDebugEnabled()) {
            SQLWarning warningToLog = stmt.getWarnings();
            while (warningToLog != null) {
                logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" +
                             warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]");
                warningToLog = warningToLog.getNextWarning();
            }
        }
    }
    else {
        handleWarnings(stmt.getWarnings());
    }
}

5、关闭资源

最终会调用到DataSourceUtilsdoReleaseConnection方法,源码如下:

public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
    if (con == null) {
        return;
    }
    if (dataSource != null) {
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && connectionEquals(conHolder, con)) {
            // 说明开启了事务,那么不会调用close方法,之后将连接的占用数减1
            conHolder.released();
            return;
        }
    }
    // 调用close方法关闭连接
    doCloseConnection(con, dataSource);
}

总结

总的来说,这篇文章涉及到的内容都是比较简单的,通过这篇文章是希望让大家对Spring中的数据访问有一定了解,相当于热身吧,后面的文章难度会加大,下篇文章我们将介绍更高级的数据访问,myBatis的使用以及基本原理、事务管理以及它跟Spring的整合原理。

如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

本文使用 mdnice 排版

查看原文

赞 0 收藏 0 评论 1

我叫DMZ 发布了文章 · 2020-07-06

面试必杀技,讲一讲Spring中的循环依赖

本系列文章:

听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译

读源码,我们可以从第一行读起

你知道Spring是怎么解析配置类的吗?

配置类为什么要添加@Configuration注解?

谈谈Spring中的对象跟Bean,你知道Spring怎么创建对象的吗?

这篇文章,我们来谈一谈Spring中的属性注入

Spring中AOP相关的API及源码解析,原来AOP是这样子的

你知道Spring是怎么将AOP应用到Bean的生命周期中的吗?

推荐阅读:

Spring官网阅读 | 总结篇

Spring杂谈

本系列文章将会带你一行行的将Spring的源码吃透,推荐阅读的文章是阅读源码的基础!

前言

Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃不掉。如果你回答得好,那么这就是你的必杀技,反正,那就是面试官的必杀技,这也是取这个标题的原因,当然,本文的目的是为了让你在之后的所有面试中能多一个必杀技,专门用来绝杀面试官!

本文的核心思想就是,

当面试官问:

“请讲一讲Spring中的循环依赖。”的时候,

我们到底该怎么回答?

主要分下面几点

  1. 什么是循环依赖?
  2. 什么情况下循环依赖可以被处理?
  3. Spring是如何解决的循环依赖?

同时本文希望纠正几个目前业界内经常出现的几个关于循环依赖的错误的说法

  1. 只有在setter方式注入的情况下,循环依赖才能解决(
  2. 三级缓存的目的是为了提高效率(

OK,铺垫已经做完了,接下来我们开始正文

什么是循环依赖?

从字面上来理解就是A依赖B的同时B也依赖了A,就像下面这样

image-20200705175322521

image-20200705175322521

体现到代码层次就是这个样子

@Component
public class A {
    // A中注入了B
 @Autowired
 private B b;
}

@Component
public class B {
    // B中也注入了A
 @Autowired
 private A a;
}

当然,这是最常见的一种循环依赖,比较特殊的还有

// 自己依赖自己
@Component
public class A {
    // A中注入了A
 @Autowired
 private A a;
}

虽然体现形式不一样,但是实际上都是同一个问题----->循环依赖

什么情况下循环依赖可以被处理?

在回答这个问题之前首先要明确一点,Spring解决循环依赖是有前置条件的

  1. 出现循环依赖的Bean必须要是单例
  2. 依赖注入的方式不能全是构造器注入的方式(很多博客上说,只能解决setter方法的循环依赖,这是错误的)

其中第一点应该很好理解,第二点:不能全是构造器注入是什么意思呢?我们还是用代码说话

@Component
public class A {
// @Autowired
// private B b;
 public A(B b) {

 }
}


@Component
public class B {

// @Autowired
// private A a;

 public B(A a){

 }
}

在上面的例子中,A中注入B的方式是通过构造器,B中注入A的方式也是通过构造器,这个时候循环依赖是无法被解决,如果你的项目中有两个这样相互依赖的Bean,在启动时就会报出以下错误:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

为了测试循环依赖的解决情况跟注入方式的关系,我们做如下四种情况的测试

依赖情况

依赖注入方式

循环依赖是否被解决

AB相互依赖(循环依赖)

均采用setter方法注入

AB相互依赖(循环依赖)

均采用构造器注入

AB相互依赖(循环依赖)

A中注入B的方式为setter方法,B中注入A的方式为构造器

AB相互依赖(循环依赖)

B中注入A的方式为setter方法,A中注入B的方式为构造器

具体的测试代码跟简单,我就不放了。从上面的测试结果我们可以看到,不是只有在setter方法注入的情况下循环依赖才能被解决,即使存在构造器注入的场景下,循环依赖依然被可以被正常处理掉。

那么到底是为什么呢?Spring到底是怎么处理的循环依赖呢?不要急,我们接着往下看

Spring是如何解决的循环依赖?

关于循环依赖的解决方式应该要分两种情况来讨论

  1. 简单的循环依赖(没有AOP)
  2. 结合了AOP的循环依赖

简单的循环依赖(没有AOP)

我们先来分析一个最简单的例子,就是上面提到的那个demo

@Component
public class A {
    // A中注入了B
 @Autowired
 private B b;
}

@Component
public class B {
    // B中也注入了A
 @Autowired
 private A a;
}

通过上文我们已经知道了这种情况下的循环依赖是能够被解决的,那么具体的流程是什么呢?我们一步步分析

首先,我们要知道Spring在创建Bean的时候默认是按照自然排序来进行创建的,所以第一步Spring会去创建A

与此同时,我们应该知道,Spring在创建Bean的过程中分为三步

  1. 实例化,对应方法:AbstractAutowireCapableBeanFactory中的createBeanInstance方法
  2. 属性注入,对应方法:AbstractAutowireCapableBeanFactorypopulateBean方法
  3. 初始化,对应方法:AbstractAutowireCapableBeanFactoryinitializeBean

这些方法在之前源码分析的文章中都做过详细的解读了,如果你之前没看过我的文章,那么你只需要知道

  1. 实例化,简单理解就是new了一个对象
  2. 属性注入,为实例化中new出来的对象填充属性
  3. 初始化,执行aware接口中的方法,初始化方法,完成AOP代理

基于上面的知识,我们开始解读整个循环依赖处理的过程,整个流程应该是以A的创建为起点,前文也说了,第一步就是创建A嘛!

image-20200706092738559

image-20200706092738559

创建A的过程实际上就是调用getBean方法,这个方法有两层含义

  1. 创建一个新的Bean
  2. 从缓存中获取到已经被创建的对象

我们现在分析的是第一层含义,因为这个时候缓存中还没有A嘛!

调用getSingleton(beanName)

首先调用getSingleton(a)方法,这个方法又会调用getSingleton(beanName, true),在上图中我省略了这一步

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

getSingleton(beanName, true)这个方法实际上就是到缓存中尝试去获取Bean,整个缓存分为三级

  1. singletonObjects,一级缓存,存储的是所有创建好了的单例Bean
  2. earlySingletonObjects,完成实例化,但是还未进行属性注入及初始化的对象
  3. singletonFactories,提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象

因为A是第一次被创建,所以不管哪个缓存中必然都是没有的,因此会进入getSingleton的另外一个重载方法getSingleton(beanName, singletonFactory)

调用getSingleton(beanName, singletonFactory)

这个方法就是用来创建Bean的,其源码如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {

            // ....
            // 省略异常处理及日志
            // ....

            // 在单例对象创建前先做一个标记
            // 将beanName放入到singletonsCurrentlyInCreation这个集合中
            // 标志着这个单例Bean正在创建
            // 如果同一个单例Bean多次被创建,这里会抛出异常
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 上游传入的lambda在这里会被执行,调用createBean方法创建一个Bean后返回
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            // ...
            // 省略catch异常处理
            // ...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 创建完成后将对应的beanName从singletonsCurrentlyInCreation移除
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 添加到一级缓存singletonObjects中
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

上面的代码我们主要抓住一点,通过createBean方法返回的Bean最终被放到了一级缓存,也就是单例池中。

那么到这里我们可以得出一个结论:一级缓存中存储的是已经完全创建好了的单例Bean

调用addSingletonFactory方法

如下图所示:

image-20200706105535307

image-20200706105535307

在完成Bean的实例化后,属性注入之前Spring将Bean包装成一个工厂添加进了三级缓存中,对应源码如下:

// 这里传入的参数也是一个lambda表达式,() -> getEarlyBeanReference(beanName, mbd, bean)
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 添加到三级缓存中
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

这里只是添加了一个工厂,通过这个工厂(ObjectFactory)的getObject方法可以得到一个对象,而这个对象实际上就是通过getEarlyBeanReference这个方法创建的。那么,什么时候会去调用这个工厂的getObject方法呢?这个时候就要到创建B的流程了。

当A完成了实例化并添加进了三级缓存后,就要开始为A进行属性注入了,在注入时发现A依赖了B,那么这个时候Spring又会去getBean(b),然后反射调用setter方法完成属性注入。

image-20200706114501300

image-20200706114501300

因为B需要注入A,所以在创建B的时候,又会去调用getBean(a),这个时候就又回到之前的流程了,但是不同的是,之前的getBean是为了创建Bean,而此时再调用getBean不是为了创建了,而是要从缓存中获取,因为之前A在实例化后已经将其放入了三级缓存singletonFactories中,所以此时getBean(a)的流程就是这样子了

image-20200706115959250

image-20200706115959250

从这里我们可以看出,注入到B中的A是通过getEarlyBeanReference方法提前暴露出去的一个对象,还不是一个完整的Bean,那么getEarlyBeanReference到底干了啥了,我们看下它的源码

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

它实际上就是调用了后置处理器的getEarlyBeanReference,而真正实现了这个方法的后置处理器只有一个,就是通过@EnableAspectJAutoProxy注解导入的AnnotationAwareAspectJAutoProxyCreator也就是说如果在不考虑AOP的情况下,上面的代码等价于:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
}

也就是说这个工厂啥都没干,直接将实例化阶段创建的对象返回了!所以说在不考虑AOP的情况下三级缓存有用嘛?讲道理,真的没什么用,我直接将这个对象放到二级缓存中不是一点问题都没有吗?如果你说它提高了效率,那你告诉我提高的效率在哪?

image-20200706124118108

image-20200706124118108

那么三级缓存到底有什么作用呢?不要急,我们先把整个流程走完,在下文结合AOP分析循环依赖的时候你就能体会到三级缓存的作用!

到这里不知道小伙伴们会不会有疑问,B中提前注入了一个没有经过初始化的A类型对象不会有问题吗?

答:不会

这个时候我们需要将整个创建A这个Bean的流程走完,如下图:

image-20200706133018669

image-20200706133018669

从上图中我们可以看到,虽然在创建B时会提前给B注入了一个还未初始化的A对象,但是在创建A的流程中一直使用的是注入到B中的A对象的引用,之后会根据这个引用对A进行初始化,所以这是没有问题的。

结合了AOP的循环依赖

之前我们已经说过了,在普通的循环依赖的情况下,三级缓存没有任何作用。三级缓存实际上跟Spring中的AOP相关,我们再来看一看getEarlyBeanReference的代码:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

如果在开启AOP的情况下,那么就是调用到AnnotationAwareAspectJAutoProxyCreatorgetEarlyBeanReference方法,对应的源码如下:

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    // 如果需要代理,返回一个代理对象,不需要代理,直接返回当前传入的这个bean对象
    return wrapIfNecessary(bean, beanName, cacheKey);
}

回到上面的例子,我们对A进行了AOP代理的话,那么此时getEarlyBeanReference将返回一个代理后的对象,而不是实例化阶段创建的对象,这样就意味着B中注入的A将是一个代理对象而不是A的实例化阶段创建后的对象。image-20200706161709829

看到这个图你可能会产生下面这些疑问

  1. 在给B注入的时候为什么要注入一个代理对象?

答:当我们对A进行了AOP代理时,说明我们希望从容器中获取到的就是A代理后的对象而不是A本身,因此把A当作依赖进行注入时也要注入它的代理对象

  1. 明明初始化的时候是A对象,那么Spring是在哪里将代理对象放入到容器中的呢?

image-20200706160542584

image-20200706160542584

在完成初始化后,Spring又调用了一次getSingleton方法,这一次传入的参数又不一样了,false可以理解为禁用三级缓存,前面图中已经提到过了,在为B中注入A时已经将三级缓存中的工厂取出,并从工厂中获取到了一个对象放入到了二级缓存中,所以这里的这个getSingleton方法做的时间就是从二级缓存中获取到这个代理后的A对象。exposedObject == bean可以认为是必定成立的,除非你非要在初始化阶段的后置处理器中替换掉正常流程中的Bean,例如增加一个后置处理器:

@Component
public class MyPostProcessor implements BeanPostProcessor {
 @Override
 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  if (beanName.equals("a")) {
   return new A();
  }
  return bean;
 }
}

不过,请不要做这种骚操作,徒增烦恼!

  1. 初始化的时候是对A对象本身进行初始化,而容器中以及注入到B中的都是代理对象,这样不会有问题吗?

答:不会,这是因为不管是cglib代理还是jdk动态代理生成的代理类,内部都持有一个目标类的引用,当调用代理对象的方法时,实际会去调用目标对象的方法,A完成初始化相当于代理对象自身也完成了初始化

  1. 三级缓存为什么要使用工厂而不是直接使用引用?换而言之,为什么需要这个三级缓存,直接通过二级缓存暴露一个引用不行吗?

答:这个工厂的目的在于延迟对实例化阶段生成的对象的代理,只有真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象

我们思考一种简单的情况,就以单独创建A为例,假设AB之间现在没有依赖关系,但是A被代理了,这个时候当A完成实例化后还是会进入下面这段代码:

// A是单例的,mbd.isSingleton()条件满足
// allowCircularReferences:这个变量代表是否允许循环依赖,默认是开启的,条件也满足
// isSingletonCurrentlyInCreation:正在在创建A,也满足
// 所以earlySingletonExposure=true
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
// 还是会进入到这段代码中
if (earlySingletonExposure) {
 // 还是会通过三级缓存提前暴露一个工厂对象
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

看到了吧,即使没有循环依赖,也会将其添加到三级缓存中,而且是不得不添加到三级缓存中,因为到目前为止Spring也不能确定这个Bean有没有跟别的Bean出现循环依赖。

假设我们在这里直接使用二级缓存的话,那么意味着所有的Bean在这一步都要完成AOP代理。这样做有必要吗?

不仅没有必要,而且违背了Spring在结合AOP跟Bean的生命周期的设计!Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

三级缓存真的提高了效率了吗?

现在我们已经知道了三级缓存的真正作用,但是这个答案可能还无法说服你,所以我们再最后总结分析一波,三级缓存真的提高了效率了吗?分为两点讨论:

  1. 没有进行AOP的Bean间的循环依赖

从上文分析可以看出,这种情况下三级缓存根本没用!所以不会存在什么提高了效率的说法

  1. 进行了AOP的Bean间的循环依赖

就以我们上的A、B为例,其中A被AOP代理,我们先分析下使用了三级缓存的情况下,A、B的创建流程

image-20200706171514327

image-20200706171514327

假设不使用三级缓存,直接在二级缓存中

image-20200706172523258

image-20200706172523258

上面两个流程的唯一区别在于为A对象创建代理的时机不同,在使用了三级缓存的情况下为A创建代理的时机是在B中需要注入A的时候,而不使用三级缓存的话在A实例化后就需要马上为A创建代理然后放入到二级缓存中去。对于整个A、B的创建过程而言,消耗的时间是一样的

综上,不管是哪种情况,三级缓存提高了效率这种说法都是错误的!

总结

面试官:”Spring是如何解决的循环依赖?“

答:Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

面试官:”为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?“

答:如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

一道思考题

为什么在下表中的第三种情况的循环依赖能被解决,而第四种情况不能被解决呢?

提示:Spring在创建Bean时默认会根据自然排序进行创建,所以A会先于B进行创建

依赖情况

依赖注入方式

循环依赖是否被解决

AB相互依赖(循环依赖)

均采用setter方法注入

AB相互依赖(循环依赖)

均采用构造器注入

AB相互依赖(循环依赖)

A中注入B的方式为setter方法,B中注入A的方式为构造器

AB相互依赖(循环依赖)

B中注入A的方式为setter方法,A中注入B的方式为构造器

如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

公众号

公众号

我叫DMZ,一个在学习路上匍匐前行的小菜鸟! 码字不易,本文要是对你有帮助的话,记得点个赞吧!

本文使用 mdnice 排版

查看原文

赞 1 收藏 0 评论 0

我叫DMZ 发布了文章 · 2020-07-05

你知道Spring是怎么将AOP应用到Bean的生命周期中的吗?

聊一聊Spring是怎么将AOP应用到Bean的生命周期中的?

本系列文章:

听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译

读源码,我们可以从第一行读起

你知道Spring是怎么解析配置类的吗?

配置类为什么要添加@Configuration注解?

谈谈Spring中的对象跟Bean,你知道Spring怎么创建对象的吗?

这篇文章,我们来谈一谈Spring中的属性注入

Spring中AOP相关的API及源码解析,原来AOP是这样子的

推荐阅读:

Spring官网阅读 | 总结篇

Spring杂谈

本系列文章将会带你一行行的将Spring的源码吃透,推荐阅读的文章是阅读源码的基础!

前言

在上篇文章中(Spring中AOP相关的API及源码解析,原来AOP是这样子的)我们已经分析过了AOP的实现的源码,那么Spring是如何将AOP应用到Bean的生命周期的呢?这篇文章就带着大家来探究下这个问题。本文我们要分析的代码还是位于org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean这个方法中,在《我们来谈一谈Spring中的属性注入 》这篇文章中,我们已经分析过了populateBean这个方法,

image-20200703202825887

image-20200703202825887

所以本文我们接着来看看initializeBean这个方法,它主要干了这么几件事

  1. 执行Aware接口中的方法
  2. 执行生命周期回调方法
  3. 完成AOP代理

对应源码如下:

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {  if (System.getSecurityManager() != null) {   AccessController.doPrivileged((PrivilegedAction<Object>) () -> {    invokeAwareMethods(beanName, bean);    return null;   }, getAccessControlContext());  }  else {                       // 执行Aware接口中的方法   invokeAwareMethods(beanName, bean);  }  Object wrappedBean = bean;  if (mbd == null || !mbd.isSynthetic()) {                        // 调用InitDestroyAnnotationBeanPostProcessor            // 的postProcessBeforeInitialization方法            // 处理@PostContructor注解标注的方法            // 另外有一部分aware方法也是在这里调用的   wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);  }  try {            // 如果实现了InitializingBean,会调用afterPropertiesSet方法            // 如果XML中配置了init-method属性,会调用对应的初始化方法   invokeInitMethods(beanName, wrappedBean, mbd);  }  catch (Throwable ex) {   throw new BeanCreationException(     (mbd != null ? mbd.getResourceDescription() : null),     beanName, "Invocation of init method failed", ex);  }  if (mbd == null || !mbd.isSynthetic()) {            // 在这里完成AOP   wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);  }  return wrappedBean; }

因为在Spring官网阅读(九)Spring中Bean的生命周期(上)文章中我们已经对这个方法做过分析了,并且这个方法本身也比较简单,所以不再对这个方法做过多赘述,我们主要关注的就是Spring是如何将AOP应用到Bean的生命周期中的,对应的就是applyBeanPostProcessorsAfterInitialization这个方法,其源码如下:

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)    throws BeansException {    Object result = existingBean;    for (BeanPostProcessor processor : getBeanPostProcessors()) {        Object current = processor.postProcessAfterInitialization(result, beanName);        if (current == null) {            return result;        }        result = current;    }    return result;}

实际上就是调用了所有后置处理器的postProcessAfterInitialization方法,在Spring中AOP相关的API及源码解析,原来AOP是这样子的一文中已经提到过了,@EnableAspectJAutoProxy注解实际上就是向容器中注册了一个AnnotationAwareAspectJAutoProxyCreator,这个类本身就是一个后置处理器,AOP代理就是由它在这一步完成的。

Bean生命周期中AOP的流程

1、@EnableAspectJAutoProxy

通过@EnableAspectJAutoProxy注解向容器中注册一个AnnotationAwareAspectJAutoProxyCreatorBeanDefinition,它本身也是一个BeanPostProcessor,这个BeanDefinition会在org.springframework.context.support.AbstractApplicationContext#registerBeanPostProcessors这个方法中完成创建,如下图所示

image-20200704112937846

image-20200704112937846

2、postProcessBeforeInstantiation方法执行

执行AnnotationAwareAspectJAutoProxyCreatorpostProcessBeforeInstantiation方法,实际上就是父类AbstractAutoProxyCreatorpostProcessBeforeInstantiation被执行

对应源码如下:

//  这个方法的主要目的就是在不考虑通知的情况下,确认哪些Bean不需要被代理//  1.Advice,Advisor,Pointcut类型的Bean不需要被代理//  2.不是原始Bean被包装过的Bean不需要被代理,例如ScopedProxyFactoryBean//  实际上并不只是这些Bean不需要被代理,如果没有对应的通知需要被应用到这个Bean上的话//  这个Bean也是不需要被代理的,只不过不是在这个方法中处理的。public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {    Object cacheKey = getCacheKey(beanClass, beanName);    // 如果beanName为空或者为这个bean提供了定制的targetSource    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {        // advisedBeans是一个map,其中key是BeanName,value代表了这个Bean是否需要被代理        // 如果已经包含了这个key,不需要在进行判断了,直接返回即可        // 因为这个方法的目的就是在实例化前就确认哪些Bean是不需要进行AOP的        if (this.advisedBeans.containsKey(cacheKey)) {            return null;        }        // 说明还没有对这个Bean进行处理        // 在这里会对SpringAOP中的基础设施bean,例如Advice,Pointcut,Advisor做标记        // 标志它们不需要被代理,对应的就是将其放入到advisedBeans中,value设置为false        // 其次,如果这个Bean不是最原始的Bean,那么也不进行代理,也将其value设置为false        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {            this.advisedBeans.put(cacheKey, Boolean.FALSE);            return null;        }    }    // 是否为这个Bean提供了定制的TargetSource    // 如果提供了定制的TargetSource,那么直接在这一步创建一个代理对象并返回    // 一般不会提供    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);    if (targetSource != null) {        if (StringUtils.hasLength(beanName)) {            this.targetSourcedBeans.add(beanName);        }        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);        this.proxyTypes.put(cacheKey, proxy.getClass());        return proxy;    }    return null;}

3、postProcessAfterInitialization方法执行

实际上也是执行父类AbstractAutoProxyCreator中的方法,对应源码如下:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {    if (bean != null) {        Object cacheKey = getCacheKey(bean.getClass(), beanName);        // 什么时候这个判断会成立呢?        // 如果不出现循环引用的话,remove方法必定返回null        // 所以这个remove(cacheKey) != bean肯定会成立        // 如果发生循环依赖的话,这个判断就不会成立        // 这个我们在介绍循环依赖的时候再详细分析,        // 目前你只需要知道wrapIfNecessary完成了AOP代理        if (this.earlyProxyReferences.remove(cacheKey) != bean) {            // 需要代理的话,在这里完成的代理            return wrapIfNecessary(bean, beanName, cacheKey);        }    }    return bean;}

4、wrapIfNecessary方法执行

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {       // 在postProcessBeforeInstantiation方法中可能已经完成过代理了    // 如果已经完成代理了,那么直接返回这个代理的对象    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {        return bean;    }        // 在postProcessBeforeInstantiation方法中可能已经将其标记为不需要代理了    // 这种情况下,也直接返回这个Bean    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {        return bean;    }        // 跟在postProcessBeforeInstantiation方法中的逻辑一样    // 如果不需要代理,直接返回,同时在advisedBeans中标记成false    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {        this.advisedBeans.put(cacheKey, Boolean.FALSE);        return bean;    }    // 获取可以应用到这个Bean上的通知    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);    // 如果存在通知的话,说明需要被代理    if (specificInterceptors != DO_NOT_PROXY) {        this.advisedBeans.put(cacheKey, Boolean.TRUE);        // 到这里创建代理,实际上底层就是new了一个ProxyFactory来创建代理的        Object proxy = createProxy(            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));        this.proxyTypes.put(cacheKey, proxy.getClass());        return proxy;    } // 如果没有通知的话,也将这个Bean标记为不需要代理    this.advisedBeans.put(cacheKey, Boolean.FALSE);    return bean;}

关于创建代理的具体源码分析,在Spring中AOP相关的API及源码解析,原来AOP是这样子的一文中已经做了详细介绍,所以本文不再赘述,现在我们的重点将放在Spring是如何解析出来通知的,对应方法就是getAdvicesAndAdvisorsForBean,其源码如下:

第一步:调用org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean

protected Object[] getAdvicesAndAdvisorsForBean(      Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {     // 通过findEligibleAdvisors方法返回对应的通知   // 这个方法回返回所有能应用在指定的Bean上的通知   List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);      if (advisors.isEmpty()) {      return DO_NOT_PROXY;   }   return advisors.toArray();}

第二步:调用org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {    // 获取到所有的通知    List<Advisor> candidateAdvisors = findCandidateAdvisors();    // 从获取到的通知中筛选出能应用到这个Bean上的通知    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);    extendAdvisors(eligibleAdvisors);    if (!eligibleAdvisors.isEmpty()) {        eligibleAdvisors = sortAdvisors(eligibleAdvisors);    }    return eligibleAdvisors;}

第三步:调用org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors获取到所有的通知

// 这个方法的目的就是为了获取到所有的通知protected List<Advisor> findCandidateAdvisors() {       // 先调用父类的方法,父类会去查找容器中所有属于Advisor类型的Bean    List<Advisor> advisors = super.findCandidateAdvisors();       // 这个类本身会通过一个aspectJAdvisorsBuilder来构建通知    // 构建的逻辑就是解析@Aspect注解所标注的类中的方法    if (this.aspectJAdvisorsBuilder != null) {        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());    }        // 最后返回这些通知    return advisors;}

第四步:org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors构建通知,这个方法比较长,我们就只分析其中的关键代码即可

public List<Advisor> buildAspectJAdvisors() {  List<String> aspectNames = this.aspectBeanNames;  if (aspectNames == null) {   synchronized (this) {    aspectNames = this.aspectBeanNames;    if (aspectNames == null) {     List<Advisor> advisors = new ArrayList<>();     aspectNames = new ArrayList<>();     // 会获取到容器中的所有BeanName     String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(       this.beanFactory, Object.class, true, false);     for (String beanName : beanNames) {      // 如果对beanName配置了正则匹配的话,那么要按照正则表达式的匹配规则进行过滤      // 默认是没有的,可以认为isEligibleBean始终返回true      if (!isEligibleBean(beanName)) {       continue;      }      // We must be careful not to instantiate beans eagerly as in this case they      // would be cached by the Spring container but would not have been weaved.      Class<?> beanType = this.beanFactory.getType(beanName);      if (beanType == null) {       continue;      }      // 判断类上是否添加了@Aspect注解      if (this.advisorFactory.isAspect(beanType)) {       aspectNames.add(beanName);       AspectMetadata amd = new AspectMetadata(beanType, beanName);       // 默认就是SINGLETON,代理切面对象是单例的       if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {                            // 最后从这个切面实例中解析出所有的通知                            // 关于通知解析的具体代码就不再分析了         MetadataAwareAspectInstanceFactory factory =          new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);        List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);        if (this.beanFactory.isSingleton(beanName)) {         this.advisorsCache.put(beanName, classAdvisors);        }        else {         this.aspectFactoryCache.put(beanName, factory);        }        advisors.addAll(classAdvisors);       }  // 省略部分代码  return advisors; }

第五步:org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findAdvisorsThatCanApply

protected List<Advisor> findAdvisorsThatCanApply(    List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {    ProxyCreationContext.setCurrentProxiedBeanName(beanName);    try {        return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);    }    finally {        ProxyCreationContext.setCurrentProxiedBeanName(null);    }}

这个方法其实没啥好分析的,就是根据前面找出来的Advisor集合进行遍历,然后根据每个Advisor对应的切点来进行匹配,如何合适就返回,对应源码也比较简单,当然前提是你看过我之前那篇AOP源码分析的文章了.

总结

这篇文章比较短,因为没有做很细节的源码分析,比较详细的源码分析已经放到上篇文章中了。最后我这里画个流程图总结一下AOP是怎么被应用到Bean的生命周期中的

image-20200705152704917

image-20200705152704917

Spring源码的最后一点补充

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)    throws BeanCreationException {    // 1.实例化    ---> createBeanInstance    // 2.属性注入  ---> populateBean    // 3.初始化    ---> 完成初始化及AOP    // exposedObject 就是完成初始化后的Bean      // 省略部分代码,省略代码的作用已经在上面标明了        // 下面的代码实际上主要目的在于处理循环依赖    if (earlySingletonExposure) {        Object earlySingletonReference = getSingleton(beanName, false);        if (earlySingletonReference != null) {            if (exposedObject == bean) {                exposedObject = earlySingletonReference;            }            // 我们之前早期暴露出去的Bean跟现在最后要放到容器中的Bean不是同一个            // allowRawInjectionDespiteWrapping为false            // 并且当前Bean被当成依赖注入到了别的Bean中            else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {                // 获取到当前Bean所从属的Bean                String[] dependentBeans = getDependentBeans(beanName);                // 要得到真实的从属的Bean                Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);                for (String dependentBean : dependentBeans) {                    // 移除那些仅仅为了类型检查而创建出来                    if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {                        actualDependentBeans.add(dependentBean);                    }                }                if (!actualDependentBeans.isEmpty()) {     // 抛出异常                    // 出现了循环依赖,并且实际存在容器中的Bean跟被当作依赖注入到别的Bean中的                    // 不是同一个对象,这个时候也报错                }            }        }    }    // 注册bean的销毁回调    try {        registerDisposableBeanIfNecessary(beanName, bean, mbd);    }    catch (BeanDefinitionValidationException ex) {        throw new BeanCreationException(            mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);    }    return exposedObject;}

实际这段代码还是跟循环依赖相关,循环依赖是Spring中一个比较重要的话题,不管是为了面试还是更好的了解清楚Spring的流程都很有必要去弄懂它

关于Spring的循环依赖,我将在下篇文章专门分析!

如果本文对你有帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

公众号

公众号

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

查看原文

赞 0 收藏 0 评论 0

我叫DMZ 发布了文章 · 2020-07-03

听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译

源码学习第一步,Spring源码编译

之所以写这么一篇文章是因为群里的小伙伴在编译源码时碰到了问题,再加上笔者自身正准备做一个源码的注释版本,恰好也需要重新编译一份代码,至于为什么要将源码编译到本地就不用多说了吧?

比如,你可以任意的添加注释,一边读源码一边记笔记,甚至你可以修改源码,更好的调试程序等等。但是如果你坚持通过导入依赖或者引入jar包的方式来学习源码,我觉得也可以,没有最好的,只有最适合自己的!

本文的主要目的是帮助那些在源码学习之初就被源码编译劝退的同学重拾信心!

话不多说,我们开始正题

参考官方文档:

https://github.com/spring-pro...

https://github.com/spring-pro...

前期准备

  1. 确保本机已经安装好了git
  2. jdk对应版本为1.8

Snipaste_2020-07-03_10-22-10

  1. Gradle,目前不需要安装,在编译的时候根据源码提示按照对应版本的Gradle即可
  2. IDEA,我使用的版本如下:

image-20200703162142721

1、获取Spring源码

这里我推荐使用clone的方式将源码拉取到本地,最大的好处在于可以利用IDEA直接比较版本间的差异,例如

在这里插入图片描述

在上图中我本地编译的5.0版本的代码,所以我对比的是5.05.1版本populateBean方法实现的差异。

接下来我们开始拉取Spring源码,大家可以按照以下几步进行

  1. 在任意磁盘路径下新建一个文件夹,名称随便取,建议为SpringFramWork
  2. 进入SpringFramWork文件夹中,打开git命令行,输入以下命令
git clone https://github.com/spring-projects/spring-framework.git

接着等待仓库克隆完毕,这个过程可能会耗费比较长的时间,如果实在不行的话,大家可以直接将源码的压缩包down下来。

clone666

如果你是跟我一样直接拉取的代码,记得切换到5.2.x版本,在命令行中执行命令:

 git checkout origin/5.2.x

2、添加阿里云镜像

在编译过程中,Spring会去自动下载一些依赖的包,默认使用的是官方的镜像,下载比较慢,所以我们提前添加好国内镜像,将下面这行代码粘贴到build.gradle文件中的repositories节点下即可 ,

//添加阿里云镜像
maven { url "http://maven.aliyun.com/nexus/content/groups/public" }

如下图所示在这里插入图片描述

3、预先编译spring-oxm模块

打开命令行窗口,并切换到源码所在文件夹,执行以下命令

gradlew :spring-oxm:compileTestJava

出现BUILD SUCCESS字样时说明构建成功,如下图所示

bulid

4、根据编译后的源码下载并安装对应的版本的Gradle

在完成对spring-oxm模块的编译后,会在当前目录生成一个.gradle文件夹,打开后可以查看对应的Gradle版本。

Snipaste_2020-07-03_15-11-3

双击打开.gradle文件夹就能看到对应所需要的gradle的版本号

Snipaste_2020-07-03_15-15-

大家直接在这个网站上下载对应所需要的版本即可:https://gradle.org/releases/,选择 binary-only

安装好后记得配置Gradle的环境变量

  • 新增 GRADLE_HOME 环境变量,指向Gradle解压目录
  • 配置Path环境变量:新增 %GRADLE_HOME%\bin

之后测试是否安装成,在命令行中输入以下命令:gradle -v,查看是否正确输出了对应版本。

image-20200703152417552

5、为安装好的Gradle配置国内镜像

进入Gradle安装目录,在init.d目录下新建一个init.gradle文件,并添加以下内容:

allprojects{
    repositories {
        def REPOSITORY_URL = 'http://maven.aliyun.com/nexus/content/groups/public/'
        all { ArtifactRepository repo ->
            def url = repo.url.toString()
            if ((repo instanceof MavenArtifactRepository) && (url.startsWith('https://repo1.maven.org/maven2') || url.startsWith('https://jcenter.bintray.com'))) {
                project.logger.lifecycle 'Repository ${repo.url} replaced by $REPOSITORY_URL .'
                remove repo
            }
        }
        maven {
            url REPOSITORY_URL
        }
    }
}

如下图所示:

gradle-init

6、将代码导入到IDEA中

6.1、打开IDEA并选择导入项目

image-20200703154502944

6.2、选择导入一个Gradle项目

image-20200703154634013

6.3、配置导入的项目

image-20200703154919932

点击Finish后等待IDEA构建完项目,如果你之前已经按照我的方法配置了Gradle的国内镜像,这个过程不会太久,我们本机只用了10多分钟就构建完成了

构建完成后整个项目结构如下:

image-20200703155239583

如果你跟我一样是直接检出的代码,记得将分支切换到5.2.x

source-checkout

7、构建整个项目

选择Build > Build Project

image-20200703162734719

可能出现的问题

AnnotationCacheAspect找不到符号

在这个过程中你可能会碰到如下错误:

20190328102605453

这是因为AnnotationCacheAspect.aj 不是java文件需要另外的aspectj 进行处理,可以按照以下步骤解决这个问题

打开命令行,cd到AspectJ的jar包所在的文件夹,运行java -jar aspectj-1.9.4.jar命令,打开AspectJ的安装界面,直接点击Next,如下图:

image-20200703164636898

接着选择jdk的安装路径,继续Next。

image-20200703164719216

接着选择AspectJ的安装路径,然后Install安装。

img

  • IDEA中配置aspectj

确保以下两个插件已经被激活

  1. Spring AOP/@AspectJ
  2. AspectJ Support

image-20200703165117938

将编译器改为 Ajc,接着设置Ajc的安装目录,选择到aspectjtools.jar,同时,一定要将Delegate to Javac选项打钩,这个代理设置的作用只对指定的项目进行Ajc编译,其他的项目还是用默认的javac编译器编译。如果不勾选这个代理选项,则全部项目都使用Ajc编译器编译,可能会导致编译错误。

image-20200703165837296

  • 指定需要使用Ajc编译的项目

分别为spring-aopspring-aspects添加Facets属性。

点击File --> Project Structure --> Facets,选择spring-aop.main,点击OK

点击File --> Project Structure --> Facets,选择spring-aspects.main,点击OK

完成添加,如下图所示:

在这里插入图片描述

完成上述步骤后,再次选择Build > Build Project ,成功完成编译

8、添加测试模块

8.1、右键工程名 ---> new ---> module

image-20200703172538821

8.2、选择Gradle及Java

image-20200703172305000

8.3、输入模块名称

image-20200703172720394

点击next ---> Finish 完成测试模块的创建

最后,添加一些必要的依赖,修改创建好的模块中的build.gradle文件,添加如下三个依赖

compile(project(":spring-aop"))
compile(project(":spring-context"))
optional("org.aspectj:aspectjweaver")

如下图所示:

在这里插入图片描述

至此,我们就完成了整个Spring的编译,并且在创建了一个日后学习使用的模块!

如果本文对你有帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

公众号

查看原文

赞 1 收藏 0 评论 0

我叫DMZ 发布了文章 · 2020-06-03

学习源码的第八个月,我成了Spring的开源贡献者

@TOC

我的经历

关注我的朋友都知道,关注两个字划重点,要考!

img

我最近一直在写Spring的文章,而且仅仅是Spring FrameWork的文章 ,从最开始的官网入门到现在源码的深度分析。主要就是三个系列

官网入门系列Spring官网读书笔记,这一系列的文章是入门Spring的不二之选,也是后续源码阅读的基础

杂谈系列Spring杂谈,这主要是一些补充内容,可以帮助大家更全面学习到Spring中的各个知识点,同时也会分享一些源码阅读技巧,个人学习心得之类的,杂谈嘛,就是不知道放哪里的文章都打算放这里,比如这篇文章。

img

源码分析系列Spring源码解析,该专栏目前正在创作中,相对而言学习难度比较大,而且因为笔者写的比较细,估计大部分同学看起来会很费劲,不过如果你能认真看完,收获绝对巨大!当然有不懂得地方也可以给笔者留言,或者关注文章末尾的公众号。

本文的主要目的是教(zhuang)学(bi)

image-20200602071958918

就是从笔者的实际经验出发,谈一谈怎么成为一个开源项目的贡献者

我先说说我自己的经历吧,在创作[上篇文章]()的时候,笔者发现Spring在实例化对象的时候有这么一段代码,在org.springframework.beans.factory.support.ConstructorResolver#resolveConstructorArguments方法中

// 本文不探讨技术细节,只是为了简单说明这个问题,所以省略无关代码    
private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
            ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {

      // ....
        for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
            int index = entry.getKey();
            if (index < 0) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                        "Invalid constructor argument index: " + index);
            }
            // 问题就出在这里
            if (index > minNrOfArgs) {
                minNrOfArgs = index + 1;
            }
       // ..... 

上述代码中,minNrOfArgs这个变量就是保存方法需要的最小参数个数,但是index是下标索引,索引是从0开始的,如果有下标为n的元素,那么最小的参数个数应该是n+1嘛,所以if中的逻辑是没有问题的,但是if这个判断是有问题的,正确的做法应该是

if (index+1 > minNrOfArgs) {
    minNrOfArgs = index + 1;
}

当发现这个问题的时候,第一反应就是肯定是我的姿势不对,错的怎么可能是代码,肯定是我!

在这里插入图片描述

接下来,我就对这段代码进行了惨无人道的调试,在无数次debug后,我发现,这个地方确实有问题!

image-20200602203349414

在确认了这个问题之后,我要思考的就是怎么把自己的想法反馈给Spring,换而言之,怎么为伟大的开源来做贡献呢?正常来要达到这个目的有两个方式

  • 提交issue
  • 直接在GitHub上提交PR(pull request)

对应的就是在GitHub上点击下图红框选中的两个位置

image-20200602204827377

如果是使用提交issue的方式,相当于给官方团队提交了一个议题,这个议题可能是你发现代码中的某个bug,也可能是你觉得官方的做法不够好,你有更好的想法等等。感兴趣的话,大家可以去看看Spring中现在有哪些还未关闭的issue,说不定其中一个你就能解决呢~!

如果要采用提交PR的方式的话,首先你得将代码fork到自己的GitHub中,然后在从自己的GitHub检出到本地,在本地做完修改后,提交到GitHub仓库中,最后从自己的GitHub向Spring官方仓库发起一个PR。

像我的话很早就已经将代码fork到了自己GitHub

image-20200602211528302

上图中的第一个红框,说明我这个仓库是从Spring官方fork过来的,第二个红框就是可以从这里向Spring官方提交一个PR。关于详细的如何提交PR,大家可以自行百度,这里不做详细的介绍了。

另外,说了这么多,先给大家看下我提交的issue吧。

issue链接:https://github.com/spring-pro...

因为内容也不长,所以我这里把原文就直接放到下面了

In ConstructorResolver:

private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
            ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {
        TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
        // ...

        for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
            int index = entry.getKey();
            if (index < 0) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                        "Invalid constructor argument index: " + index);
            }
            if (index > minNrOfArgs) {
                minNrOfArgs = index + 1;
            }
            // ....
        }
// ....
 return minNrOfArgs;
}

I assume that method resolveConstructorArguments is to resolve contructor arguments in the XML file and return the minimum number of parameters required by contructor 。but if the first parameter is autowired , the second parameter is config by XML file,the method will not work well。

example:

public class FactoryObject {
    
 public DmzService getDmz(String name, int age, Date birthDay, OrderService orderService) {

    public DmzService getDmz(OrderService orderService,String name) {
        
        return new DmzService(orderService,name);
    }

}
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="constructor">
    <bean id="factoryObject" class="com.dmz.spring.first.instantiation.service.FactoryObject"/>

    <bean class="com.dmz.spring.first.instantiation.service.OrderService" id="orderService"/>

    <bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz">
        <constructor-arg index="1"  value="dmz"/>
    </bean>

</beans>

the resolveConstructorArguments method will return 1,but correct answer is 2。

I think the problem arises because of this judgment:

if (index > minNrOfArgs) {
 minNrOfArgs = index + 1;
}

It might be better to change it to look like this

if (index + 1 > minNrOfArgs) {
 minNrOfArgs = index + 1;
}s

我在提交issue时主要是按照这种思路

  1. 首先摆出有问题的代码
  2. 描述具体的问题,我是直接通过一个例子来描述的
  3. 说出自己的建议

这几天我又多看了看别人提交的issue,对比起来,我觉得至少应该还要添加一点

  • 应该要明确的指出具体哪个版本上出现的问题

碰到的问题

1、担心闹乌龙

虽然在之前我已经调试过了无数次代码,但是心里还是没谱啊。毕竟我这么谨(cai)慎(ji)的一个人,万一被人喷了怎么办?不知道你会不会这么想,反正我当时就是这么想的,如果你是这么想的,建议你去看看别人提交的issue。搜索条件如下

image-20200602221719579

is:closed label:"status: invalid"

我觉得你看几个,自然就有信心了!

2、不知道要怎么提交

每个开源的项目,只要作者希望这个项目越来越好的话,都会详细的说明如何给这个项目做开源贡献,Spring肯定也不例外,这里还是以提交issue为例,当你点击New issue的时候会出现下面这张图

image-20200602222421975

在上图左边的框里很明确的告诉了你提交issue应该要注意什么

  • 首先,你应该要去Stack Overflow提问
  • 如果是bug,你应该要指明版本以及你想要做什么
  • 如果是一个增强的话,要提供上下文并且描述清楚问题
  • 同一个问题,issue跟PR最好只提交一个,因为GitHub认为它们是一样的,如果你还不能确定的话,先提交一个issue

而右上角还有更加详细的文档可供参考。

3、英文

大家应该看到了,整个issue都是用英文写的,那么英文不好怎么办呢?这个时候就要掏出我们的神器了

image-20200602214339837

嗯,就是词典,笔者习惯是使用有道词典。我建议英文不好的同学可以这样,先将整个issue用中文写好,如果你真的英文一窍不通的话,可以直接通过翻译软件逐句翻译,然后粘贴到GitHub上。但是千万千万不要使用中文,就像下面这个哥们

issue链接:https://github.com/spring-pro...

image-20200602215146927

像这种issue是会被直接打上invalid(不合格)标签的,你就想想吧,你学不会英文,你指望我们的外国朋友能看懂中文嘛?是我中华上线五千年的文化不够博大精深吗?

4、担心问题描述的不清楚

其实这个问题就是因为英文不好衍生出来的。因为英文不好,自然就会担心我写的东西他能不能看懂呢?我的建议就是,结合你测试的代码去描述问题。你不用去担心别人看不懂你写的代码,就以我那个issue的处理流程为例吧。

image-20200602215944850

在你刚刚提交issue时,有专门的issuemaster(issue管理员)会给你提交的issue打上一个wait-for-triage的标签,标志这个issue是待处理的。

随后我提交的这个issue,就被指派给了jhoeller。你要担心他看不懂代码吗?给你看两个东西吧

image-20200602220527438

你知道那个红框是啥意思吗?就是说我发现的那个有问题代码的类的作者就是他。

再看一张

image-20200602220833491

就是说,jhoeller从2003年开始就已经是Spring这个项目的管理者以及发布经理了。2003年,我还是一个小学生........

菜

所以啊,只要你稍微正常点,基本上人家都能get到你的点。

给你的建议

其实笔者从发现这个问题到最终提交issue大概经过了一周时间,期间一直在犹豫要不要提交issue,就是因为上面提到的几个问题,一直踌躇不前。但是等我下定决心要去做这件事的时候总共就花了几个小时的时间。包括研究issue提交的规则以及写一篇英文版的issue。并且我提交issue的第二天就马上被处理了,并且jhoellerf9aae8d 这个commit中已经接受我的建议。

所以我要说的就是,

真正动手的话,不管什么问题总能找到解决方案

而只是停留在空想,在踌躇,你永远有一堆问题

临渊羡鱼,不如退而结网

以此文与君共勉!

如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。

公众号

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 0 收藏 0 评论 0

我叫DMZ 关注了专栏 · 2020-06-02

前端 1943

前端技术文章

关注 2465

我叫DMZ 关注了用户 · 2020-06-02

编程码农 @onlythinking

Life is so short, do something to make yourself happy, such as coding.

关注 544

我叫DMZ 关注了专栏 · 2020-06-02

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 55402

我叫DMZ 关注了专栏 · 2020-06-02

码农田小齐

一起玩转算法和数据结构呀~ 公众号:码农田小齐

关注 2286

认证与成就

  • 获得 3 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-06-02
个人主页被 282 人浏览