SpringBoot 启动分析(三) — Environment 的初始化流程

1. Environment 的初始化流程

ConfigFileApplicationListener 收到 ApplicationEnvironmentPreparedEvent 事件后通过 SPI 加载所有的 EnvironmentPostProcessor 实现,触发其 postProcessEnviroment 方法。

SpringApplication.run() ->
SpringFactoriesLoader.loadFactories(ApplicationListener) ->
SpringApplication.prepareEnviroment() -> EventPublishingRunListener.enviromentPrepared(ApplicationEnviromentPraparedEvent) ->
SimpleApplicationEventMulticaster.multicastEvent() ->
ConfigFileApplicationListener.onApplicationOnEnviromentPreparedEvent() ->
EnviromentPostProcessor.postProcessEnviroment()

比较重要的 EnviromentPostProcessor 实现是 HostInfoEnvironmentPostProcessorConfigFileApplicationListener

2. HostInfoEnvironmentPostProcessor.postProcessEnviroment

获取本机的 主机名和IP地址,封装在 PropertySource 添加到 environment 里。

3. ConfigFileApplicationListener.postProcessEnviroment

ConfigFileApplicationListener 自身也实现了 EnvironmentPostProcessor,通过内部类 Loader 去加载配置文件,其主要流程如下:

  1. 从 Environment 中获取 active 和 include 的 profile 集合。进行迭代:
  2. 获取所有的搜索路径,进行迭代,默认的搜索路径是 classpath:/,classpath:/config/,file:./,file:./config/
  3. 如果某个搜索路径不以 / 结尾的则认为是一个文件,直接加载,否则,找出所有的搜索文件名 name 进行迭代搜索,默认的搜索文件名是 “application”。
  4. 通过 PropertySourcesLoader 找出支持的所有配置文件后缀进行迭代。
  5. 最终得到 location + name + "-" + profile + "." + ext 组成的一个具体的完整路径,通过 PropertiesLoader.load 方法加载该路径指向的配置文件。
  6. PropertiesLoader.load 内部又根据配置文件的后缀用不同的 PropertySourceLoader 去加载得到一个 PropertySource
  7. 对于解析得到的 PropertySource,找出里面激活的 profile,添加到 proflie 集合里进行迭代。
  8. 继续迭代下一个 profile 。

继续阅读

SpringBoot 启动分析(一)

SpringBoot 启动分析 序列文章基于 spring-boot-starter-parent 1.5.19.RELEASE 。

1. 启动一个 SpringBoot 应用

启动一个 SpringBoot 应用只需要下面几行代码即可:

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

查看 SpringApplication.run 方法时会来到:

public static ConfigurableApplicationContext run(Object source, String... args) {
    return run(new Object[] { source }, args);
}

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
    return new SpringApplication(sources).run(args);
}

可以看到 SpringBoot 的魔法就是那么简单:创建一个 SpringApplication,执行其 run 方法
就像 “打开冰箱门、把大象塞进冰箱、关上冰箱门” 那么简单、有力。

当然,要了解原理是不能只看高层抽象的。

2. SPI 机制 SpringFactoriesLoader

SpringFactoriesLoader 是 Spring 提供的 SPI 实现机制,从类路径下的 META-INF/spring.factories 文件里加载指定接口的所有实现。以接口的完整类名作为 key,实现类的完整名字作为值,多个实现类用 , 分隔。

下面是 spring-boot-autoconfigure 包下 META-INF/spring.factories 文件里的 一小部分:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\

SpringFactoriesLoader 内部通过 ClassLoader.getResources 方法来加载类路径下的文件。

继续阅读

SpringBoot 启动分析(二)–启动主流程

1. initialize 方法

SpringApplication 的构造函数只调用了 initialize 方法:

private void initialize(Object[] sources) {
    if (sources != null && sources.length > 0) {
        // 把应用指定的配置类加入配置扫描的启动来源
        this.sources.addAll(Arrays.asList(sources));
    }

    // 判断应用是否是 web 应用,主要用于决定采用哪种具体的上下文实现
    this.webEnvironment = deduceWebEnvironment();

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationContextInitializer 的所有实现并设置到 initializers 属性
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationListener 的所有实现并设置到 listeners 属性
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

    // 找出启动类:线程栈上 main 方法所处的类
    this.mainApplicationClass = deduceMainApplicationClass();
}

该方法的逻辑主要如下:

  1. 把启动类加入 sources 属性。
  2. 判断是否是 web 应用并设置到 webEnvironment 属性。
  3. 加载所有的 ApplicationContextInitializer 并设置到 initializers 属性。
  4. 加载所有的 ApplicationListener 并设置到 listeners 属性。
  5. 找出 main 类设置到 mainApplicationClass。

2. run 方法

继续阅读

应用事务管理混乱导致的一个坑

Spring 的事务传播属性

org.springframework.transaction.annotation.Propagation 定义了 Spring 的事务传播属性:

  • REQUIRED: 支持当前事务,如果不存在则新建一个。

  • REQUIRES_NEW: 创建新的事务,如果当前存在一个则 suppend 当前的。

  • SUPPORTS: 支持当前事务,如果不存在则以非事务方式执行。

  • MANDATORY: 支持当前事务,如果不存在则抛出异常。

  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在一个事务则 suppend 当前的。

  • NEVER: 以非事务方式执行,如果存在事务则抛出异常。

  • NESTED: 如果当前存在一个事务则以嵌套事务的方式执行。

一个生产问题

一开始是 DBA 反馈数据库出现两种现象:

  1. 出现一些操作做完但会话一直还在等待客户端的提交动作。
  2. 偶尔出现大量的行锁,导致 JVM 线程互相等待而假死。

有个获取流水号的方法 systemService.getSerialNo 的事务传播属性是 NOT_SUPPORTED 的,这个方法通过类似这样的 update t_serialno set serial_no = :newSerialNo where serial_key = :key and serial_no = :oldSerialNo SQL 语句进行更新,更新返回的受影响行数等于 1 认为新的流水号是不重复的,更新不成功则重试。

行锁就出现在这些流水号的更新上。

继续阅读

Spring 事务管理的一个 trick

问题

最近有同事碰到这个异常信息: Transaction rolled back because it has been marked as rollback-only ,异常栈被吃了,没打印出来。

调用代码大概如下:

@Component
public class InnerService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Throwable.class)
    public void innerTx(boolean ex) {
        jdbcTemplate.execute("insert into t_user(uname, age) values('liuwhb', 31)");
        if (ex) {
            throw new NullPointerException();
        }
    }

}

@Component
public class OutterService {
    @Autowired
    private InnerService innerService;

    @Transactional(rollbackFor = Throwable.class)
    public void outTx(boolean ex) {
        try {
            innerService.innerTx(ex);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

outterService.outTx(true);

他期望的是 innerService.innerTx(ex); 调用即使失败了也不会影响 OutterService.outTx 方法上的事务,只回滚了 innerTx 的操作。

结果没有得到他想要的,调用 OutterService.outTx 的外围方法捕获到了异常,异常信息是 Transaction rolled back because it has been marked as rollback-onlyoutTx 的其他操作也没有提交事务。

分析

上述方法的事务传播机制的默认的,也就是 Propagation.REQUIRED,如果当前已有事务就加入当前事务,没有就新建一个事务。

事务性的方法 outTx 调用了另一个事务性的方法 innerTx 。调用方对被调用的事务方法进行异常捕获,目的是希望被调用方的异常不会影响调用方的事务。

但还是会影响调用方的行为的。Spring 捕获到被调用事务方法的异常后,会把事务标记为 read-only,然后调用方提交事务的时候发现事务是只读的,就会抛出上面的异常。

继续阅读

接口与Spring自动注入

接口与自动注入

有业务接口 IService 和两种业务逻辑的实现 Aservice, Bservice 如下:

public interface IService {}

@Component
public class Aservice implements IService {}

@Component
public class Bservice implements IService {}

采用下面的方式自动注入时,会报错:

public class ManageService {
    @Autowired
    private Aservice aservice;

    @Autowired
    private Bservice bservice;

    // ...
}

异常信息类似为:nested exception is java.lang.IllegalArgumentException: Can not set xxx.ManageService field xxx.ManageService.aservice to com.sun.proxy.$Proxy48

继续阅读

Spring AOP 与 事务实现

1. AOP 与方法调用

对于 接口 interface,可以通过 java.lang.reflect.Proxy 来生成动态代理对象;
对于 类 class,可以通过字节码技术,如 CBLIB 来生成一个子类作为代理对象,此时类不能声明为 final 。

当 Spring 把 Bean 注入到另一个 Bean 时,其实注入的是它生成的代理对象,进行方法调用时,首先调用的是代理对象上的方法,代理对象的方法最终再调目标对象的方法,也就是开发人员编写的方法。Spring 通过在调用目标方法前后做处理来实现一些特性,例如事务管理、缓存等。

Spring 的 AOP 是基于对代理对象的方法调用的拦截的,只能拦截外部对象 对 某个对象的方法的调用,对象的方法调用同一个类的方法是不会被拦截的。

@Service
public class SpringTxService {
    private static final Logger LOGGER = LoggerFactory
            .getLogger(SpringTxService.class);

    @Transactional(propagation = Propagation.REQUIRED)
    public void add() {
        LOGGER.info("do add");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requireNew() {
        LOGGER.info("do requireNew");
    }

    public void composite() {
        add();
        requireNew();
    }
}

上面的代码是基于注解进行事务控制的,composite 方法没有声明事务属性,它会调用 add、 requireNew
方法。当把 SpringTxService 注入到另一个 BeanB:
1. 在 BeanB 的方法里调用代理对象 composite 时,最终执行 add、 requireNew 是没有在事务里执行的,只是普通的方法调用。
2. 在 BeanB 的方法里调用代理对象的 add、 requireNew 方法时,这两个方法都会分别在相应的事务里执行。

当发现基于 AOP 实现的特性没有预期的效果时,一定要看看是不是在代理对象上调用还是目标类内部的方法调用。

继续阅读

Spring 导入资源文件

Spring 有多种方式可以导入资源文件,可以把这些属性文件里的属性值注入到 bean 的属性上。

1. 通过 <context:property-placeholder> 标签导入

通过这种方式导入的所有属性在同一个命名空间下,如果多个属性文件里有相同的属性名,以先导入的属性文件的属性为准,不会出现覆盖。可以通过 @Value("${propName}") 的方式注入到 bean 的属性上。

这种方式不能对属性文件指定 id,没法引用指定属性文件的属性,如果有同名的属性,引用到的总是第一个导入的属性。

配置如下:

<!-- 导入外部的资源文件,可以通过 @Value("${propName}") 的方式注入到 bean 的属性上 。 -->
<context:property-placeholder
    location="classpath:/learn/properties/1.properties"
    ignore-unresolvable="true" />
<!-- 要使用 context:property-placeholder 导入多个资源文件,必须指定 ignore-unresolvable="true" -->
<!-- 多个属性文件中,属性名相同的,以第一个加载的为准,不会出现后面加载的覆盖前面的 -->
<context:property-placeholder location="learn/properties/2.properties"
    ignore-unresolvable="true" />

继续阅读

最近处理的两个坑:Spring 启动问题与 log4j 配置

Spring 依赖注入问题

近半年,在生产环境老是出现应用在 JBoss 里启动不来,在下面的方法输出日志、进入循环后就走不出那个循环了:

Spring 3.0.5: org.springframework.beans.factory.support.DefaultListableBeanFactory

public void preInstantiateSingletons() throws BeansException {
     if (this.logger.isInfoEnabled()) {
          this.logger.info("Pre-instantiating singletons in " + this);
     }

     synchronized (this.beanDefinitionMap) {
          for (String beanName : this.beanDefinitionNames) {
               RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
               if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                    if (isFactoryBean(beanName)) {
                         final FactoryBean factory = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName);
                         boolean isEagerInit;
                         if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                              isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
                                   public Boolean run() {
                                        return ((SmartFactoryBean) factory).isEagerInit();
                                   }
                              }, getAccessControlContext());
                         }
                         else {
                              isEagerInit = factory instanceof SmartFactoryBean && ((SmartFactoryBean) factory).isEagerInit(); 
                         }
                         if (isEagerInit) {
                              getBean(beanName);
                         }
                    }
                    else {
                         getBean(beanName);
                    }
               }
          }
     }
}

因为是生产环境,没法直接远程 debug,只能 dump 出栈和堆来分析,从栈来看,Spring 一直在做 bean 实例化;从堆来看,是这样的:

Top elements include:

•13,701 × Error creating bean with name ‘sqlSessionFactory’ … > (496 bytes)
•13,732 × Could not autowire method: public final void org.m… (336 bytes)
…….//还有很多其他的类创建失败

这个问题有一定的随机性,因为不是总是起不来,重启多次之后就可能顺利启动了,搞得每次发版本都胆战心惊。

继续阅读

Spring MVC 与 web开发

项目组用了 Spring MVC 进行开发,觉得对里面的使用方式不是很满意,就想,如果是我来搭建开发环境,我会怎么做?下面就是我的想法,只关注于 MVC 的 View 层。

一、统一的响应格式

现在基本上都是用 ajax 来调用后台接口,拿到 json格式的数据再展示,有的人直接返回数据,却没有考虑异常的情况,我觉得返回的报文里必须包含表示可能的异常信息的数据和业务响应数据。我定义了下面这个类来表示报文格式:

/**
 * 统一的 HTTP 响应格式。<br/>
 * code 为 "ok" 表示业务调用成功,否则是失败的错误码,如果有多个则以逗号分隔。<br/>
 * data 是业务数据,如果失败了则是 null。
 * 
 * @author http://coderbee.net
 *
 */
public class RespBody {
    public static final String OK_CODE = "ok";
    private final String code;
    private final Object data;

    private static final RespBody OK = new RespBody(OK_CODE, null);

    private RespBody(String code, Object data) {
        this.code = code;
        this.data = data;
    }

    public static RespBody ok() {
        return OK;
    }

    public static RespBody ok(Object data) {
        return new RespBody("ok", data);
    }

    public static RespBody error(String code) {
        return new RespBody(code, null);
    }

    public static RespBody error(String code, Object msg) {
        return new RespBody(code, msg);
    }

    public String getCode() {
        return code;
    }

    public Object getData() {
        return data;
    }
}

这个类提供了一些静态方法来快速构建响应报文,这也是很重要的一个设计:用静态工厂方法而不是构造函数。

这里的 code 不应该是直接的错误提示信息,应该只是简单的错误编码,这样不同的客户端都可以调用这个 API,然后再根据错误编码、客户端语言和自己的客户端特性选择合适的错误提示信息和提示方式。

继续阅读