[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.activespring.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;
}
posted @ 2019-10-06 22:13  影灵衣  阅读(419)  评论(0编辑  收藏  举报