[spring] SpEL
简介
我们书写的程序中,各个类之间有依赖的,需要手动实例化依赖类再赋给它。既然我们都通过IoC容器自动管理Bean了,每次使用都需要自己管理这样的依赖关系过于繁琐。
于是就有了通过配置文件的方式,使其自动注入依赖的bean。
spring提供3种装配的方式:xml装配
,java装配
,自动装配
。
相比于
xml装配
,推荐的是使用java装配
。
通常使用自动装配
,减少配置文件。
自动装配
spring从2个角度实现自动装配:
组件扫描
:spring会自动发现应用上下文中所创建的bean。自动装配
:spring自动将满足的bean装配。
项目结构
.
├── build.gradle
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── yww/
│ │ ├── Main.java
│ │ ├── Message.java
│ │ ├── ReaderConfig.java
│ │ └── Reader.java
│ └── resources/
│ └── beans.xml
└── test/
├── java/
│ └── com/
│ └── yww/
│ └── MainTest.java
└── resources/
代码
build.gradle
项目构建配置。
注释的部分是使用插件并直接
gradle run
命令运行项目。jar{}
部分的配置,可以打包成一个用java
命令运行的jar
包。(注意需要指定好主类的全名)
plugins {
id 'java'
// id 'application'
}
// mainClassName = 'com.yww.Main'
group 'com.yww'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
ext{
springVersion = '5.2.0.RELEASE'
}
dependencies {
compile "org.springframework:spring-core:$springVersion"
compile "org.springframework:spring-context:$springVersion"
compile "org.springframework:spring-beans:$springVersion"
compile "org.springframework:spring-expression:$springVersion"
compile "org.springframework:spring-aop:$springVersion"
compile "org.springframework:spring-aspects:$springVersion"
testCompile "junit:junit:4.12"
testCompile "org.springframework:spring-test:$springVersion"
}
jar {
from {
configurations.runtime.collect{zipTree(it)}
}
manifest {
attributes 'Main-Class': 'com.yww.Main'
}
}
Message.java
通过使用@Component
注解,在组件扫描时,注册bean。
@Value
注解简单直接赋值。不同于xml配置文件配置bean这么繁琐。
package com.yww;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Message {
@Value("---hello world---")
private String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = "this mssage is : " + msg;
}
}
Reader.java
依赖于Message.java
,使用@Autowired
注入Message
这个bean到Reader
中。
此处可以将
@Autowired
写在成员变量上,也可写在构造函数上,二者选其一皆可。(构造函数自动连线可以做一些其它操作而已)
package com.yww;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Reader {
// @Autowired
private Message msg;
@Autowired
public Reader(Message msg){
this.msg = msg;
}
public void print(){
System.out.println(msg.getMsg());
}
}
ReaderConfig.java
配置文件,主要时用于在获取应用上下文时,设置此类可以完成开启其中的配置。其类名任意。这里主要是开启组件扫描,将默认同包名下带有@Component
等注解的类注册为bean。
习惯上,我们可以在此处做一些配置工作,比如设置初始值,或者如何装配。为了方便,初始值在前面已使用
@Value
设置。
package com.yww;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class ReaderConfig {
// 可以在配置文件中设置初始值
// 为了方便直接,就在Message的成员上使用@Value设置了值。
// @Bean
// public Message message(){
// Message message = new Message();
// message.setMsg("---hi---");
// return message;
// }
}
Main.java
主函数,用于启动应用。这里展示如何获取到有依赖关系,并且按照自动装配好的bean。
不同于web应用,app需要有个主函数启动应用,常规的web应用会打包成war放入tomcat加载,不过
spring boot
的web应用也可以有个主函数启动web应用。
代码里注释的部分,是使用
xml配置
,演示如何获取bean。
package com.yww;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args){
// xml配置(beans.xml)
// ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// Message msg = (Message) context.getBean("msg1");
// System.out.println(msg.getMsg());
// java配置(ReaderConfig.java) + 获取组件扫描到的bean
// 测试文件AppTest.java中,可以通过注解@ContextConfiguration加载配置文件上下文,方便使用@Autowired注解获取Bean。
ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.ReaderConfig.class);
Reader reader = (Reader) context.getBean(Reader.class);
reader.print();
}
}
最后剩下测试文件MainTest.java
,这里不多过问,只是可以使用@ContextConfiguration
注解获取上下文的bean,在使用@Autowired
自动注入bean,方便了测试。
运行
因为为了简单,项目是非web项目,很少有帖子讲解对此的打包和运行。(web项目就很容易通过添加
war
插件和gradle build
打包)
运行的方法尝试出了几种:
-
方法1:使用idea直接运行
main
类即可,也可运行测试类MainTest.java
。 -
方法2:使用gradle,需要在
build.gradle
文件中,添加插件application
,并设置好主函数的名称mainClassName
。最后在项目根目录下(build.gradle
同级目录)执行命令:
gradle run
问题处理
在
非web
应用中,会发现一个问题,无法通过@Autowired
获取到bean,这是由于非web应用无法知道bean,也没有提供相应的注解去处理,只能通过ApplicationContext
应用上下文获取bean。而bean之间是可以通过@Autowired
获取到的。
java装配
很多时候,通过组件扫描和自动装配实现自动化配置都是推荐的方式。但如果要将第三方库装配到自己的应用中,就无法使用@Component
注解实现自动化装配。
要想显式配置bean,可以在配置文件中使用@Bean
获取。
// src/main/java/com/yww/ReaderConfig.java
// ...
@Bean
public Message message(){
Message message = new Message();
message.setMsg("---hi---");
return message;
}
// ...
使用自动装配,不能创建多个同类型的bean。
Bean更名
默认bean的ID与@Bean
注解的方法名一样的。
可提供name
参数设置其它名称。
@Bean(name="msgX")
不使用组件扫描的配置
如果不使用组件扫描,那么一个配置如何找到另一个配置,或者bean?
此时,可以使用@Import
导入那个类。
package com.yww;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({MessageConfig.class})
public class ReaderConfig {
// 可以在配置文件中设置初始值
// 为了方便直接,就在Message的成员上使用@Value设置了值。
// @Bean
// @Bean(name="msgX")
// public Message message(){
// Message message = new Message();
// message.setMsg("---hi---");
// return message;
// }
// 这个bean传入的名称可以任意.
// 但如果在同一个类里声明了名称,像上面那样指定了名词,这里的参数名也必须相同。
@Bean
public Reader reader(Message msg){
System.out.println(msg.getMsg());
msg.setMsg("---hello---");
return new Reader(msg);
}
}
其中,导入的另一个bean的java配置
文件如下。
// src/main/java/com/yww/MessageConfig.java
package com.yww;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageConfig {
@Bean
public Message message(){
return new Message();
}
}
xml装配
- gradle app的
xml配置
放在src/main/resources/
目录下。 - web的
xml配置
放在web/WEB-INF/
目录下,需要在web.xml
中配置<context-param>
。
配置形如下面这个。
<?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/
<bean id="msg1" class="com.yww.Message">
<property name="msg" value="hello world"/>
</bean>
</beans>
如果不给id
,将会通过全限定类名来命名,此处的bean将会被命名为com.yww.Message#0
,#0
是计数同类型的其它bean。
其它设置
由于不推荐使用xml配置
,故只是简单在此记下有哪些配置。
很多配置有
c-命名空间
,p-命名空间
去替代繁杂的标签。
- 构造其注入bean引用,注入字符串,数字,列表等。(<constructor-arg>)
- 使用xml配置<bean>导入JavaConfig配置。
<beans>
的profile
属性。<jdbc>
,<jee:jndi-lookup>
。<beans>
嵌套<beans>
。
高级装配
提供更多配置,实现更多装配的控制。
Profile
通常我们需要配置不同的环境,如:测试环境使用嵌入数据库h2
,生产环境使用jndi
获取数据库等。
(从spring4开始)
@Profile
也是基于@Conditional
实现。
设置配置
可以使用@Profile
注解在类
或方法
上,去生成Bean。(此处使用JavaConfig
配置,也可以使用xml
配置)
package com.yww;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan
@PropertySource("application.properties")
public class ReaderConfig {
@Bean
@Profile("dev")
public Message message1(){
Message message = new Message();
message.setMsg("---hi---1");
return message;
}
@Bean
@Profile("prod")
public Message message2(){
Message message = new Message();
message.setMsg("---hi---2");
return message;
}
}
@PropertySource
导入配置文件。
激活配置
通过两个属性确定哪个profile激活,spring.profiles.active
和spring.profiles.default
。
如果设置了active
,就用此值确定哪个profile激活。如果没设置active
,就用default
确定。如果都没设置,只会创建没有定义profile的bean。
#src/main/resources/application.properties
spring.profiles.default=dev
spring.profiles.active=prod
在测试类中,可以使用
@ActiveProfiles("dev")
便捷的激活配置。
其它方式
有多种方式设置属性:
- DispatcherServlet的初始化参数。
- Web应用的上下文参数。
- JNDI条目。
- 环境变量。
- JVM的系统属性。
- 集成测试类上,使用@ActiveProfiles注解设置。
例,web.xml
的配置。
<!-- ... -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<!-- ... -->
附:配置文件是如何关联的?
程序如何找到这个.properties
文件?
在app项目中,我们通过@PropertySource
注解到JavaConfig类上,设置.properties
配置文件的路径。
在gradle项目中,配置文件放在
src/main/resources/
路径下,还可以放在这个目录下的文件夹。如:src/main/resources/demo/app.properties
的设置@PropertySource("demo/app.properties")
。
在web项目中,spring web已经将配置文件设置好了,不需要@PropertySource
配置。
条件化的Bean
如果希望实现一个bean只有在应用的类路径下包含特定的库时才创建,或者希望某个bean只有当在另一个特定bean声明之后才会创建,或者在设置了特定的环境变量后才会创建某个bean。
通过@Conditional
注解,它可以用于带有@Bean
注解的方法上。如果条件计算为true就会创建bean,否则这个bean就会被忽略。
条件化bean是
spring boot
自动装配的实现原理。
示例
当配置文件中含有"reader"值时,创建bean。
目录结构
.
├── build.gradle
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── yww/
│ │ ├── Main.java
│ │ ├── Message.java
│ │ ├── ReaderConfig.java
│ │ ├── ReaderExistsCondition.java
│ │ └── Reader.java
│ └── resources/
│ ├── application.properties
└── test/
├── java/
│ └── com/
│ └── yww/
│ └── MainTest.java
└── resources/
关键代码
先取消了Reader.java
类的@Component
注解,方便此处使用条件化方式创建Reader的bean。
设置的条件为ReaderExistsCondition.java
,这个类继承接口Condition
。
package com.yww;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan
@PropertySource("application.properties")
public class ReaderConfig {
@Bean
public Message message1(){
Message message = new Message();
message.setMsg("---hi---");
return message;
}
@Bean
@Conditional(ReaderExistsCondition.class)
public Reader reader(Message message){
return new Reader(message);
}
}
设置的条件为,从配置文件中寻找是否存在名为reader
的配置属性,如果方法matches
返回true,即会创建被设置条件的bean。
package com.yww;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class ReaderExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("reader");
}
}
附:接口Condition
Condition
接口源码如下,只包含一个matches
方法,如果方法返回true
即会满足条件创建bean,false
即不会创建。
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
ConditionContext
接口可以检查Bean,环境变量,资源,类。
AnnotatedTypeMetadata
接口能检查带有@Bean
注解的方法上还有什么其它注解。
处理自动装配的歧义性
当同一个类型有多个bean被创建,组件扫描无法选取来装配。
spring会抛出
NoUniqueBeanDefinitionException
。
解决方法
有2种处理方法:
- 在
@Component
或@Bean
上,使用@Primary
注解,在遇到歧义时,选择首选的bean。 - 在
@Autowired
或@Inject
上,使用@Qualifier
限定要注入的bean的ID
。当注解到@Component
或@Bean
时,则是创建自己的限定符(类似于更名bean的ID)。
@Qualifier
限定名类似于设置标签给bean,spring会装载满足标签的bean。
为了设置多个标签,可以自己定义注解了
@Qualifier
的接口,用自定义注解作为标签限定。(java 8允许在定义注解上添加@Repeatable
实现重复注解,不过Spring的@Qualifier
并没有为此添加重复注解功能。)
附:byName和byType
一般情况下,虽然创建的bean和注入的参数名不一样(此处的bean名message1
和参数名message
),但spring会以byType
的方式找到类型相同的bean装配好。
package com.yww;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan
public class ReaderConfig {
@Bean
public Message message1(){
Message message = new Message();
message.setMsg("---hi---1");
return message;
}
@Bean
public Reader reader(Message message){
return new Reader(message);
}
}
但有多个同类型的bean时,就无法依赖byType
找到对应的bean,可以通过改变传入的参数名(此处改用参数名message2
),采用byName
指定bean。
package com.yww;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan
public class ReaderConfig {
@Bean
public Message message1(){
Message message = new Message();
message.setMsg("---hi---1");
return message;
}
@Bean
public Message message2(){
Message message = new Message();
message.setMsg("---hi---2");
return message;
}
@Bean
public Reader reader(Message message2){
return new Reader(message2);
}
}
注入外部值
可以通过3种方式获取环境值:
- 注入并使用
Environment
。 - 属性占位符(property placeholder)。
- Spring表达式语言
SpEL
。
#src/main/resources/application.properties
info.message=this is a message.
info.counter=10
Environment:
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
@Component
@PropertySource("application.properties")
public class Message {
@Autowired
Environment env;
public void print(){
String msgStr = env.getProperty("info.message", "this is a msg");
int msgInt = env.getProperty("info.counter", Integer.class, 30);
}
}
属性占位符:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@PropertySource("application.properties")
public class Message {
@Value("${info.message}")
private String msg;
}
Spring EL:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@PropertySource("application.properties")
public class Message {
@Value("#{systemProperties['info.message']}")
private String msg;
}