2019-04-25 22:54:07 +0000   |     spring java gradle   |   Viewed times   |    

3.1 通过Java Config配置Profile

profile_31包结构如下,

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch03
    │   │                   └── profile_31
    │   │                       ├── SayHello.java
    │   │                       ├── SayHelloFromDev.java
    │   │                       ├── SayHelloFromProd.java
    │   │                       └── config
    │   │                           └── ProfileConfig.java
    │   └── resources
    │       └── log4j.properties
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch03
                            └── profile_31
                                └── SayHelloTest.java

实验中在ProfileConfig配置类中用@Bean注解手动创建SayHelloFromDevSayHelloFromProd类的bean实例各一个。分别用@Profile("dev")@Profile("prod")标注。这两个类实现了同一个接口SayHello

测试类SayHelloTest中用@Autowired让spring自动分配一个SayHello类型的bean实例。

@Autowired
private SayHello sayHelloBot;

@ActiveProfiles("dev")注解激活dev环境的情况下,spring自动选择了SayHelloFromDev类实例。改成@ActiveProfiles("prod")就会自动配置成SayHelloFromProd类实例。

3.2 @Conditional有条件地创建Bean实例

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch03
    │   │                   └── condition_32
    │   │                       ├── MagicBean.java
    │   │                       ├── condition
    │   │                       │   └── MagicExistCondition.java
    │   │                       └── config
    │   │                           └── ConditionConfig.java
    │   └── resources
    │       ├── env.properties
    │       └── log4j.properties
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch03
                            └── condition_32
                                └── ConditionTest.java

config/ConditionConfig.java配置类中用@Bean手动创建MagicBean类bean实例,同时加上@Conditional(MagicExistCondition.class)注解,Spring在创建bean实例之前会调用condition/MagicExistCondition.java类的matches()函数,若判定为真,才创建实例。

具体判定魔法是否存在,我们在Environment中设置一个环境变量magic,只有当magic = ON的时候,才会创建MagicBean单例。配置环境变量,只要在配置类ConditionConfig上加上@PropertySource("classpath:env.properties),让他去src/main/resources/env.properties配置文件,其中配置了magic=ONmatches()函数要获得Environment实例引用可以通过matches()函数的参数ConditionContext.getEnvironment()拿到。

condition_32包因为没有激活@Profile,所以不会和profile_31包的测试上下文混起来。每个测试类,比如profile_31/SayHelloTest.javacondition_32/ConditionTest.java都有各自的上下文ApplicationContext实例。

3.3 @Qualifier实验

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch03
    │   │                   └── qualifier_33
    │   │                       ├── Dessert.java
    │   │                       ├── IceCream.java
    │   │                       ├── Popsicle.java
    │   │                       ├── config
    │   │                       │   └── QualifierConfig.java
    │   │                       └── qualifier
    │   │                           ├── Cold.java
    │   │                           ├── Creamy.java
    │   │                           └── Fruity.java
    │   └── resources
    │       └── log4j.properties
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch03
                            └── qualifier_33
                                └── QualifierTest.java

有2种甜点,冰淇淋IceCream和棒冰Popsicle。冰淇淋是又@Cold@Creamy。棒冰是又@Cold@Fruity@Cold@Creamy@Fruity三个新注释都在qualifier包下定义。同样在config/QualifierConfig.java配置类手动声明了一个冰淇淋单例和一个棒冰单例。最后在QualifierTest.java测试类中,用@Autowired执行自动装配2个Dessert实例。要求其中一个同时满足又冰又奶,另一个又冰又水果味。分别只有一个满足条件的bean单例,顺利自动装配完成。

3.4 @Scope作用域

@Scope注解标明某个bean的作用域。比如@Scope(value=WebApplicationContext.SCOPE_SESSION)说明这个bean在一个Session过程中都有效。目前书中没有涉及到Session的内容,这一节例子略过。@Scope可以和自动识别@Component和手动标注@Bean一起使用。

3.5 Spring表达式语言

表达式或算式放到#{ ... }里即可。可以做数学计算,可以做条件运算,可以用作字面量,也可以用T()括起来调用某种具体java类型。例子太多,这里不一一列举。以后用到就知道了。

Logging

Spring的日志系统面向Jakarta Commons Logging API (JCL),打包在spring-jcl依赖包中。JCL和slf4j一样,不是一个实际的日志系统实现,只是一个日志接口。负责将代码和具体的日志系统,如log4jjava.util.logging解耦。

JCL的优点是它有一个自动搜索类路径下可用日志实现的功能。只需要将log4j加入依赖,项目日志就可以工作。缺点是这个搜索算法会影响效率。更多的项目用的是slf4j而不是JCL。

在Spring里如果要用slf4j替代commons-logging

  1. 先要禁用spring-jcl
  2. 再添加jcl-over-slf4j包,把之前对commons-logging的调用全转到slf4j接口上来。
  3. 再加入slf4j-api -> slf4j-log4j12 -> log4j全家桶。

gradle中配置如下,

// log4j & slf4j
configurations.all { // 不禁用spring-jcl,无法嫁接到slf4j上
    exclude group: 'org.springframework', module: 'spring-jcl'
}
implementation 'org.slf4j:jcl-over-slf4j:1.7.26'
implementation "org.slf4j:slf4j-api:1.7.26"
implementation 'org.slf4j:slf4j-log4j12:1.7.26'
implementation "log4j:log4j:1.2.17"

简单说明一下,spring-jcl包依赖由spring-core包传递进来。项目只引用了spring-context包依赖,spring-core会跟随spring-context包传递进来。

+--- org.springframework:spring-context:5.1.6.RELEASE
|    +--- org.springframework:spring-aop:5.1.6.RELEASE
|    |    +--- org.springframework:spring-beans:5.1.6.RELEASE
|    |    |    \--- org.springframework:spring-core:5.1.6.RELEASE
|    |    |         \--- org.springframework:spring-jcl:5.1.6.RELEASE
...
...

简单说一下jcl-over-slf4j的原理

Spring面向commons-logging,也就是代码里会导入下面的库。代码中对LogLogFactory的调用不可逆转地会指向commons-logging的库。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

为了劫持对org.apache.commons.logging.Log的引用,就必须写一个和commons-logging一模一样的包结构。实际上jcl-over-slf4j就是这么干的,它的包结构如下,彻底伪装成commons-logging。所以用jcl-over-slf4j的时候,必须禁掉对commons-logging的依赖,否则编译器不知道加载谁了。

org
└── apache
    └── commons
        └── logging
            ├── Log.class
            ├── LogConfigurationException.class
            ├── LogFactory.class
            └── impl
                ├── NoOpLog.class
                ├── SLF4JLocationAwareLog.class
                ├── SLF4JLog.class
                ├── SLF4JLogFactory.class
                ├── SimpleLog$1.class
                └── SimpleLog.class

jcl-over-slf4j顺利劫持原本对commons-logging的引用到slf4j之后,直接上slf4j-api -> slf4j-log4j12 -> log4j全家桶就行了。

详细过程参考下面这张图, to-use-slf4j-in-spring

解决标准输出无法在终端输出的问题

我的log4j.properties已经配置了将console扩展器定向到标准输出,但此时运行gradle构建,终端不会有输出。

# Configure stdout
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
... ...
... ...

问题出在gradle。gradle会把写到标准输出STANDARD_OUT的所有内容重定向到它的日志系统的QUIET级别中,把标准错误STANDARD_ERROR的内容重新定向到ERROR级别。gradle一共有6个级别,QUIET是仅次于ERROR的第二高级别。而gradle默认的日志级别是LIFECYCLE,比QUITE要低一级。也就是说默认情况标准输出和标准错误的内容都应该正常输出。 log-levels

但是gradle的日志容器还加了额外一道锁。负责这个日志容器配置的是test.testLogging字段, gradle-testlogging-1 gradle-testlogging-2

它是一个TestLoggingContainer类,其中的showStandardStreams参数默认为不显示标准输出的内容。 showStandardStreams

所以想要在控制台输出标准输出,还需要修改这个showStandardStreams参数。在build.gradle中可以这么修改,

test {
    testLogging {
		showStandardStreams = true    // 显示标准输出和标准错误的内容
	}
}

一般我们还会把异常跟踪栈内容显示量设置为full,以便在测试时获得尽可能多的信息。下面是一个不错的惯用配置,

test {
    testLogging {
		outputs.upToDateWhen {false}  // 就算test没有更新内容,仍然输出    
		showStandardStreams = true    // 显示标准输出和标准错误的内容
        exceptionFormat 'full'        // 显示所有异常跟踪栈内容
        events 'started', 'skipped', 'passed', 'failed'     // 记录特定测试事件
	}
}

参考文献