Spring-IOC高级

在Spring-IOC基础中我们学习了核心的bean装配技术,但是bean装配所涉及的领域并不仅仅局限于Spring-IOC基础中我们学习的内容,Spring提供了多种技巧,借助他们可以实现更为高级的bean装配功能

环境与profile

Spring提供了profile解决多个环境之间的切换问题

  • JavaConfig中使用profile
    在Java配置中可以使用@Profile注解指定某个bean属于哪个profile,@Profile注解源码如下:
package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Profiles;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();

}

为了测试效果,我们可以定义一个DataSource类如下:

package com.tp.datasource;

/**
 * FileName: DataSource
 * Author:   TP
 * Description:
 */
public class DataSource {

    public DataSource() {
        System.out.println("无参构造:DataSource被实例化");
    }

    private String userName;

    private String passWord;

    private String url;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public String toString() {
        return "DataSource{" +
                "userName='" + userName + '\'' +
                ", passWord='" + passWord + '\'' +
                ", url='" + url + '\'' +
                '}';
    }
}

通过JavaConfig声明这个bean:

@Bean("dataSource")
@Profile("dev")
DataSource devDataSource(){
    DataSource dataSource = new DataSource();
    dataSource.setUserName("root");
    dataSource.setUserName("tp123456");
    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_dev");
    return dataSource;
}

@Bean("dataSource")
@Profile("test")
DataSource testDataSource(){
    DataSource dataSource = new DataSource();
    dataSource.setUserName("root");
    dataSource.setUserName("tp123456");
    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_test");
    return dataSource;
}

@Bean("dataSource")
@Profile("prod")
DataSource prodDataSource(){
    DataSource dataSource = new DataSource();
    dataSource.setUserName("root");
    dataSource.setUserName("tp123456");
    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jdbcStudy_prod");
    return dataSource;
}

如上我们分别为dev、test、prod环境设置了对应的数据源配置,测试类如下:

public class ProfileTestMain {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ctx.getEnvironment().setActiveProfiles("dev");
        System.out.println("=============================");
        ctx.refresh();
        Object dataSource = ctx.getBean("dataSource");
        System.out.println("=============================");
        System.out.println(dataSource);
    }
}

测试结果:

=============================
无参构造:DataSource被实例化
=============================
DataSource{userName='tp123456', passWord='null', url='jdbc:mysql://127.0.0.1:3306/jdbcStudy_dev'}

Process finished with exit code 0

由此可以看出,dev环境的DataSource被实例化了

  • Spring XML中使用profile
    在Spring xml中使用profile你可以选择为每个环境设置一个单独的xml配置文件,在xml的<beans> 标签下指定profile属性,类似这样:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"
       profile="test">

       <!--针对test环境的配置信息-->
        <!--...-->
</beans>

这样配置下来可能配置文件从命名看来显得更明确,但是引发的问题是重复定义会很多。
另外一种方式是在<bean></beans>内部再添加<bean profile="dev"></beans>,这样你就可以不必为每个环境单独创建xml配置文件了:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="course" class="com.tp.beans.Course">
        <property name="name" value="Java"/>
        <property name="durations" value="24"/>
        <property name="id" value="222"/>
    </bean>

    <!--测试多环境-->
    <beans profile="dev">
        <bean id="dataSource"
              class="com.tp.datasource.DataSource"
              p:userName="root"
              p:passWord="tp123456"
              p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_dev"/>
    </beans>

    <beans profile="test">
        <bean id="dataSource"
              class="com.tp.datasource.DataSource"
              p:userName="root"
              p:passWord="tp123456"
              p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_test"/>
    </beans>

    <beans profile="prod">
        <bean id="dataSource"
              class="com.tp.datasource.DataSource"
              p:userName="root"
              p:passWord="tp123456"
              p:url="jdbc:mysql://127.0.0.1:3306/jdbcStudy_prod"/>
    </beans>
</beans>

再次运行上面的测试类,效果是一致的。

条件化的Bean

假设你希望一个bean或者多个bean在某种特定条件下才被创建,例如应用的类路径下包含特定的库才创建,或者某个bean在另一个bean已经存在时才被创建,在Spring4之前很难实现这种级别的条件化配置,Spring4之后引入了@Conditional注解,它可以用到带有@Bean注解的方法上,如果给定的条件计算为true,那么Spring就会为我们创建这个bean,否则这个bean会被忽略。

@Conditional可以给定一个任意实现了org.springframework.context.annotation.Condition的类,Condition的源码如下

@FunctionalInterface
public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     * or {@link org.springframework.core.type.MethodMetadata method} being checked
     * @return {@code true} if the condition matches and the component can be registered,
     * or {@code false} to veto the annotated component's registration
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

这个接口只有一个matches()方法,返回一个boolean类型表示是否满足条件。
为了测试条件化的bean,我们定义3个类实现Condition接口,用来判断当前系统属于哪种操作系统,并为不同的操作系统创建不同的bean:

package com.tp.conditon;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * FileName: LinuxCondition
 * Author:   TP
 * Description:
 */
public class LinuxCondition  implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
    }
}
package com.tp.conditon;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * FileName: MacCondition
 * Author:   TP
 * Description:
 */
public class MacCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").toLowerCase().contains("mac os x");
    }
}
package com.tp.conditon;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * FileName: WindowsCondition
 * Author:   TP
 * Description:
 */
public class WindowsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
    }
}

我们有个类Cat如下:

package com.tp.beans.condition;

/**
 * FileName: Cat
 * Author:   TP
 * Description:用于条件注解的bean
 */

public class Cat {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                '}';
    }
}

在JavaConfig声明bean如下:

@Bean("cat")
@Conditional(MacCondition.class)
Cat macCat(){
    Cat cat = new Cat();
    cat.setName("Mac Cat");
    return cat;
}

@Bean("cat")
@Conditional(WindowsCondition.class)
Cat windowsCat(){
    Cat cat = new Cat();
    cat.setName("Windows Cat");
    return cat;
}

@Bean("cat")
@Conditional(LinuxCondition.class)
Cat linuxCat(){
    Cat cat = new Cat();
    cat.setName("Linux Cat");
    return cat;
}

测试:

package com.tp.test;

import com.tp.beans.condition.Cat;
import org.junit.Test;

/**
 * FileName: OnConditionalBeanStatement
 * Author:   TP
 * Description:基于特定条件的JavaBean声明测试
 */
public class OnConditionalBeanStatementTest extends BaseTestCase{

    @Test
    public void conditionalBeanTest(){
        Cat cat = (Cat) applicationContext.getBean("cat");
        System.out.println(">>>基于JavaConfig的JavaBean声明:" + cat);
    }
}

运行结果:


由于本人的操作系统是Mac,所以从控制台可以看出为我们创建类Mac对应的bean。
Condition接口的matches()方法会得到ConditionContext和
AnnotatedTypeMetadata这两个对象,我们可以通过这两个对象用来做决策,其中ConditionContext是一个接口,源码如下:

public interface ConditionContext {

    /**
     * Return the {@link BeanDefinitionRegistry} that will hold the bean definition
     * should the condition match.
     * @throws IllegalStateException if no registry is available (which is unusual:
     * only the case with a plain {@link ClassPathScanningCandidateComponentProvider})
     */
    BeanDefinitionRegistry getRegistry();

    /**
     * Return the {@link ConfigurableListableBeanFactory} that will hold the bean
     * definition should the condition match, or {@code null} if the bean factory is
     * not available (or not downcastable to {@code ConfigurableListableBeanFactory}).
     */
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();

    /**
     * Return the {@link Environment} for which the current application is running.
     */
    Environment getEnvironment();

    /**
     * Return the {@link ResourceLoader} currently being used.
     */
    ResourceLoader getResourceLoader();

    /**
     * Return the {@link ClassLoader} that should be used to load additional classes
     * (only {@code null} if even the system ClassLoader isn't accessible).
     * @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
     */
    @Nullable
    ClassLoader getClassLoader();

}

通过ConditionContext,我们可以做到如下几点:

  • 借助getRegistry()方法返回的BeanDefinitionRegistry检查bean定义
  • 借助getBeanFactory()方法返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性
  • 借助getEnvironment()方法返回的Environment检查当前应用所在的运行环境信息检查环境变量是否存在
  • 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源
  • 借助getClassLoader()返回的ClassLoader加载并检查类是否存在

AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上个还有什么其他的注解,像AnnotatedTypeMetadata一样AnnotatedTypeMetadata也是一个接口:

public interface AnnotatedTypeMetadata {
    boolean isAnnotated(String var1);

    @Nullable
    Map<String, Object> getAnnotationAttributes(String var1);

    @Nullable
    Map<String, Object> getAnnotationAttributes(String var1, boolean var2);

    @Nullable
    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);

    @Nullable
    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}
  • 借助isAnnotated()方法我们可以判断带有@Bean注解的方法是不是还有其他特定的注解
  • 借助其他的那些方法我们能够检查@Bean注解的方法上其他注解的属性。

其实前面介绍的@Profile也是基于@Conditional实现的

处理自动装配的歧义性

在IOC基础的学习中我们知道Spring的自动装配能够为我们减少显式配置的数量,不过仅有一个bean匹配所需的结果时,自动装配才是有效的,如果有不止一个bean能够匹配结果的话,这种歧义性会阻碍Sprig自动装配属性、构造器参数、方法参数。
歧义性例子:
为了演示这种歧义性,我们新建一个类AnimalServiceImpl2,同AnimalServiceImpl一样,它也实现了上节课的AnimalService接口
具体如下:

@Service
public class AnimalServiceImpl2 implements AnimalService {

    @Override
    public void eat() {
        System.out.println("AnimalServiceImpl2:动物在吃饭....");
    }
}

我们的User类中引入了AnimalService接口:

@Component("componentUser")
public class User {

    public User() {
        System.out.println("无参构造:User被实例化");
    }

    // 构造器注入
    // @Autowired
    public User(Book book) {
        System.out.println("带参构造:User被实例化");
        this.book = book;
    }

    public User(String name, String[] favorites) {
        System.out.println("带参构造:User被实例化");
        this.name = name;
        this.favorites = favorites;
    }

    private Integer id;
    private String name;
    private Integer age;
    private String[] favorites;
    private Book book;

    // 接口注入
    @Autowired
    private AnimalService animalService;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String[] getFavorites() {
        return favorites;
    }

    public void setFavorites(String[] favorites) {
        this.favorites = favorites;
    }

    public Book getBook() {
        return book;
    }

    // Setter注入
    // @Autowired
    public void setBook(Book book) {
        this.book = book;
    }

    // 非Setter注入
    // @Autowired
    public void injectBook(Book book) {
        this.book = book;
    }

    public AnimalService getAnimalService() {
        return animalService;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", favorites=" + Arrays.toString(favorites) +
                ", book=" + book +
                '}';
    }
}

这个时候我们再运行一次之前正常的测试类:

/**
 * 打开User成员变量上的@Autowired注解,演示属性注入
 */
@Test
public void simpleComponentScanInterfaceInject() {
    User user = (User) applicationContext.getBean("componentUser");
    System.out.println(">>>基于注解扫描的JavaBean声明:" + user);
    System.out.println(">>>基于注解扫描的JavaBean声明,注入的book:" + user.getBook());
    System.out.println(user.getAnimalService());
    if(null != user.getAnimalService()){
        user.getAnimalService().eat();
    }
}

发现报错 :

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.tp.service.AnimalService' available: expected single matching bean but found 2: animalServiceImpl,animalServiceImpl2
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:221)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1229)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1171)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:593)
    ... 42 more

大概意思是期望只有一个匹配的bean,但是Spring在容器中找到了2个符合条件的bean,因此报错。
发生这个问题的原因是,@Autowired注解是按照类型注入,因为AnimalServiceImpl和AnimalServiceImpl2都实现了AnimalService这个类,所以Spring不知道用哪个好,解决的办法有3种:

设置首选Bean(不推荐)

我们可以利用@Primary注解将其中一个bean设置为首选的bean:

@Service
@Primary
public class AnimalServiceImpl2 implements AnimalService {

    @Override
    public void eat() {
        System.out.println("AnimalServiceImpl2:动物在吃饭....");
    }
}

这个时候再次运行测试类,效果如下:

但是这种方式是不推荐的,@Primary只能设置优先,但是无法保证唯一,因为可能多个bean中有不止一个bean设置了@Primary,那样的话程序还是会发生错误

使用@Qualifier配合@Autowired

@Autowired
@Qualifier("animalServiceImpl2")
private AnimalService animalService;

@Qualifier注解是使用限定符的主要方式,它可以与@Autowired和@Inject注解协同使用,@Qualifier注解的参数就是想要注入的bean的ID
在JavaConfig中,@Qualifier也可以与@Bean一起使用
再次运行测试类:

使用@Resource注解

@Resource有2个参数:name、type,它默认按照名称进行装配
@Resource装配顺序

  • 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
  • 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
  • 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
  • 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配

修改User.java:

@Resource(name = "animalServiceImpl2")
private AnimalService animalService;

运行测试类:

bean的作用域

默认情况下,Spring上下文中的beaen都是以单例(singleton)的形式创建的,也就是说不管给定的一个bean被注入到其他bean多少次,每次注入的都是同一个实例。
但是在某些情况下你使用的类是易变的,他们会保持一些状态,因此重用是不安全的,这种情况下将class声明为单例bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题,Spring针对了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例,Spring默认的作用域
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例
  • 请求(Request):在Web应用中,为每个请求创建一个bean实例
如何设置bean的作用域?
  • 组件扫描声明bean
    如果我们使用组件扫描声明的bean,那么我们可以使用注解@Scope("xxx")来指定bean的作用域
    @Scope的值我们可以用ConfigurableBeanFactory提供的常量,当然也可以自己手写
    例如声明为原型:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Component("componentUser")
public class User {

}
  • xml声明bean
    在XMl中声明bean的时候,我们可以使用<bean>元素的scope属性来设置:
<!--构造器注入:构造器参数为bean情形-->
<bean id="xmlUser0" class="com.tp.beans.componentscan.User" scope="prototype">
    <constructor-arg name="book" ref="javaConfigBook"/>
</bean>
  • JavaConfig声明bean
@Bean("javaConfigBook")
@Scope("prototype")
public Book book() {
    Book book = new Book();
    book.setId(222);
    book.setPrice(33.88);
    book.setName("简书");
    return book;
}

运行时值注入

Spring提供了2种在运行时求值的方式:

  • 属性占位符(Property placeholder)
  • Spring表达式(SpEL)

1.注入外部的值

在Spring中处理外部值最简单的方式是声明属性源并通过Spring的Environment来检索属性
声明属性源可以使用@PropertySource注解,其值为属性文件路径信息,例如:

@Configuration
@PropertySource("classpath:config/app.properties")
public class ValueInjectConfig {

    @Autowired
    Environment environment;

    @Bean
    public Jaguar jaguar(){
        Jaguar jaguar = new Jaguar();
        jaguar.setName(environment.getProperty("jaguar.name"));
        jaguar.setColor(environment.getProperty("jaguar.color"));
        return jaguar;
    }
}

Jaguar.java:

package com.tp.beans.valueinject;

/**
 * FileName: Jaguar
 * Author:   TP
 * Description:
 */
public class Jaguar {

    private String name;

    private String color;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Jaguar{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

在上述代码中,我们使用@PropertySource注解并指定类属性源为类路径下的config/app.properties文件,文件内容如下:

jaguar.name=捷豹XEL
jaguar.color=white
civic.name=思域
civic.color=yellow

这个文件会被加载到Spring的Environment中,我们就可以借助Environment的getPropery()方法获取出对应的值,Environment的getPropery()方法有4种:

  • String getPropery(String key);
  • String getPropery(String key, String defaultValue);
  • T String getPropery(String key, Class<T> type);
  • T String getPropery(String key, Class<T> type, T defaultValue);
    运行测试类:
public class ValueInjectTest extends BaseTestCase {

    /**
     * "@Properties"注解测试
     */
    @Test
    public void propertyResourceInjectTest() {
        Jaguar jaguar = (Jaguar) applicationContext.getBean("jaguar");
        System.out.println(">>>>@PropertyResource:" + jaguar);
    }
}

测试结果:

2.解析属性占位符

直接从Environment中获取属性是比较方便的,特别是在Java配置中装配bean的时候,但是Spring也为我们提供了占位符装配bean属性的方法,这些占位符的值来源于一个属性源,为了使属性占位符生效,我们需要配置一个PropertySourcesPlaceholderConfigurer,它能够基于Spring Environment及其属性源来解析占位符,我们可以在Spring的配置文件中配置PropertySourcesPlaceholderConfigurer:

<context:property-placeholder location="classpath*:/config/*.properties" 
                              file-encoding="utf-8"/>

准备就绪后,我们分别以注解扫描和xml形式声明一个bean

  • xml声明bean使用属性占位符:
<bean id="xmlPlaceholderJaguar" class="com.tp.beans.valueinject.Jaguar">
    <property name="name" value="${jaguar.name}"/>
    <property name="color" value="${jaguar.color}"/>
</bean>

测试类:

/**
 * 属性占位符xml形式测试
 */
@Test
public void xmlPlaceholderInjectTest() {
    Jaguar jaguar = (Jaguar) applicationContext.getBean("xmlPlaceholderJaguar");
    System.out.println(">>>>@PropertyPlaceholder of xml:" + jaguar);
}

结果:

  • 组件扫描声明bean使用占位符
package com.tp.beans.valueinject;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * FileName: Honda
 * Author:   TP
 * Description:
 */
@Component
public class Civic {

    @Value("${civic.name}")
    private String name;

    @Value("${civic.color}")
    private String color;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Civic{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

测试类:

@Test
public void scanPlaceholderInjectTest() {
    Civic civic = (Civic) applicationContext.getBean("civic");
    System.out.println(">>>>@PropertyPlaceholder of componentScan:" + civic);
}

运行结果:

SpEl

Spring 3引入了Spring表达式(Spring Expression Language),它能够以一种强大和简洁的方式将值装配到bean的属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算到值。
SpEl拥有很多特性,包括:

  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算术、关系和逻辑运算
  • 正则表达式匹配
  • 集合操作

SpEL表达式需要放在:#{}之中,形为#{xxx},这与属性占位符类似,属性占位符为${...}之中。

SpEl用法示例

  • 表示字面值
    #{3.1415926}表示一个浮点值
    #{'Hello'}表示一个String类型的字面值
    #{false}表示一个boolean类型的值
  • 引用bean属性、方法
    SpEl能够通过ID获取到这个bean,我们就可以使用SpEL对bean的属性或者方法实现引用
    例如:
    1.获取属性
    #{jaguar.name}:这个表达式会获取ID为jaguar的这个bean的name属性
    2.方法引用
    #{jaguar.getSlogan()}:这个表达式会获取ID为jaguar的这个bean,并返回其getSlogan()方法的返回值
    对于方法的返回值我们还可以继续调用返回值的函数,例如:#{jaguar.getSlogan().toUpperCase()},当然方法的返回值可能为null,这样就有可能引发空指针问题,为了避免空指针问题,我们可以使用类型安全的运算符:#{jaguar.getSlogan()?.toUpperCase()},这里我们使用了"?",这个运算符能够在访问它右边的内容之前确保左侧内容不为null,如果为null则不会执行后面的内容,直接返回null。
  • 在表达式中使用类型
    如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符,例如:为了在SpEL中使用Java的Math类,需要按照如下的方式使用T()运算符:T(java.lang.Math),此时T()运算符会得到一个Class对象,代表了java.lang.Math,假如你想将PI值装配到bean的属性中,可以这样写:
    ${T(java.lang.Math).PI},当然我们不仅可以获取访问目标的常量,我们还可以调用其方法:${T(java.lang.Math).random()*10}
  • SpEL运算符
    SpEL提供了多种运算符,这些运算符可以用在SpEL表达式上:
运算符类型 运算符
算术运算 +、-、*、/、%、^
比较运算符 <、>、==、<=、>=、lt、gt、eq、le、ge
逻辑运算符 and、or、not、|
条件运算符 三元运算符
正则表达式 matches
  • 计算集合
    SpEL还可以操作集合和数组
    例如:#{jukebox.songs[4].tile},这个表达式会计算jukebox这个bean的songs属性(是个集合,集合装的Song这个javabean)中第五个元素Song的title属性,[]用来从集合或者数组中按照索引获取数据

抛出几个简单的测试类,其他用法测试读者自行测试吧:

package com.tp.beans.valueinject;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * FileName: Benz
 * Author:   TP
 * Description:用于测试SpEl的实体
 */
@Component
public class Benz {

    //SpEL引用bean属性
    @Value("#{xmlSpelBenz.name}")
    private String name;

    //SpEL引用bean方法
    @Value("#{xmlSpelBenz.getColor()}")
    private String color;

    //SpEL表达式中使用类型
    @Value("#{T(java.lang.Math).random()*10}")
    private int carAge;

    //字面常量
    @Value("#{'买奔驰吗?这次不漏油哦~'}")
    private String slogan;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getCarAge() {
        return carAge;
    }

    public void setCarAge(int carAge) {
        this.carAge = carAge;
    }

    public String getSlogan() {
        return slogan;
    }

    public void setSlogan(String slogan) {
        this.slogan = slogan;
    }

    @Override
    public String toString() {
        return "Benz{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                ", carAge=" + carAge +
                ", slogan='" + slogan + '\'' +
                '}';
    }
}

app.properties:

jaguar.name=捷豹XEL
jaguar.color=white
civic.name=思域
civic.color=yellow
benz.slogan=买奔驰吗?漏油的那种!!!

applicationContext.xml:

<!--############################SpEL############################-->
<bean id="xmlSpelBenz" class="com.tp.beans.valueinject.Benz">
    <property name="name" value="C260L"/>
    <property name="color" value="富士白"/>
    <property name="carAge" value="#{T(Math).random()*10}"/>
    <property name="slogan" value="${benz.slogan}"/>
</bean>

测试类:

//=========================SpEl=========================
@Test
public void xmlSpelInjectTest() {
    Benz benz = (Benz) applicationContext.getBean("xmlSpelBenz");
    System.out.println(">>>>SpEL of xml:" + benz);
}


@Test
public void componentScanSpelInjectTest() {
    Benz benz = (Benz) applicationContext.getBean("benz");
    System.out.println(">>>>SpEL of componentScan:" + benz);
}

测试结果:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容