• 高级装配


    环境与profile

    在开发软件的时候,将应用程序从开发环境,迁移到测试环境,或者是迁移到生产环境都是一项挑战。最常见的就是对于DataSource的配置,这三种环境我们会根据不同的策略来生成DataSource bean。我们需要有一种方式来配置DataSource,使其在每种环境下都会选择对应的配置。

    配置profile bean

    要使用profile,首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到不同环境中,要确保对应的profile文件处于激活状态。

    在Java配置中,使用@Profile注解指定某个bean属于哪一个profile。

    import javax.sql.DataSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
    
    @Configuration
    @Profile("dev")
    public class DevelopProfileConfig {
        
        @Bean(destroyMethod="shutdown")
        public DataSource dataSource() {
            return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .addScript("classpath:scheam.sql")
                    .addScripts("classpath:test-data.sql")
                    .build();
        }
    
    }

    在这里@profile注解是应用在类级别了。它告诉Spring这个配置类中的bean只有在dev profile 激活时在会创建。如果dev profile没有激活的话,那么这个配置类下的bean会被忽略。

    如果我们为每一个配置环境都去配置一个配置类,那无疑会增加配置类的个数,不利于维护。从Spring3.2开始,就可以在方法级别上使用@profile注解,与@Bean注解一同使用。这样的话,我们就可以将多个bean放在一个配置类中了。

    import javax.sql.DataSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
    import org.springframework.jndi.JndiObjectFactoryBean;
    
    @Configuration
    public class DataSourceConfig {
        
        @Bean(destroyMethod="shutdown")
        @Profile("dev")
        public DataSource embeddedDataSource() {
            return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .addScript("classpath:scheam.sql")
                    .addScript("classpath:test-data.sql")
                    .build();
        }
        
        @Bean
        @Profile("prod")
        public DataSource jndiDataSource() {
            JndiObjectFactoryBean jndiObjectFactoryBean=new JndiObjectFactoryBean();
            jndiObjectFactoryBean.setJndiName("jdbc/myDS");
            jndiObjectFactoryBean.setResourceRef(true);
            jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
            return (DataSource) jndiObjectFactoryBean.getObject();
        }
    
    }

    只有在对应的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终会被创建,与激活哪个profile无关。

    在XML配置中,我们可以使用<beans>元素的profile属性,在XML中配置profile bean。

    在这里使用beans元素中嵌套定义beans元素的方式,而不是为每个环境都创建一个profile XML文件,这样将所有的profile bean定义在同一个XML文件中

    profile.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:jdbc="http://www.springframework.org/schema/jdbc"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:jee="http://www.springframework.org/schema/jee"
        xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
            http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <beans profile="dev">
            <jdbc:embedded-database id="datasource">
                <jdbc:script location="classpath:scheam.sql"/>
                <jdbc:script location="classpath:test-data.sql"/>
            </jdbc:embedded-database>
        </beans>
        
        <beans profile="qa">
            <bean id="datasource" class="org.apach.commons.dbcp.BesicDataSource" 
            destroy-method="close"
            p:url="jdbc:h2:tcp://dbserver/~/test"
            p:driverClassName="org.h2.Driver"
            p:username="sa"
            p:password="password"
            p:initialSize="20"
            p:maxActive="30"
            />
        </beans>
        
        <beans profile="prod">
            <jee:jndi-lookup id="datasource" jndi-name="jdbc/myDatabase"
            resource-ref="true"
            proxy-interface="javax.sql.DataSource"
            ></jee:jndi-lookup>
        </beans>
    
    </beans>

    激活profile

    Spring在确定哪个profile处于激活状态时,需要依赖两个属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active就会根据它的值确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,Spring就会去找spring.profiles.default的值.如果这两个属性都没有设置,那就没有激活的profile。

    可以有多种方式设置这两个属性:

    • 作为DispatcherServlet的初始化参数
    • 作为Web应用的上下文参数
    • 作为JNDI条目
    • 作为环境变量
    • 作为JVM的系统属性
    • 在集成测试类上,使用@ActiveProfiles注解设置

    我在这里使用前两种方式,前两种方式可以直接在web.xml中设置:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
        version="2.5">
    
        <servlet>
            <servlet-name>springDispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--         <init-param>
                <param-name>spring-profiles.default</param-name>
                <param-value>dev</param-value>
            </init-param>     -->    
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>springDispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/applicationContext.xml</param-value>
        </context-param>
        
        <context-param>
            <param-name>spring.profiles.defaule</param-name>
            <param-value>dev</param-value>
        </context-param>
        
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    
    </web-app>

    当应用部署到其他环境时,我们根据情况再来设置spring.profiles.active即可.spring.profiles.active的设置优先使用。可以注意到spring.profiles.active和spring.profiles.default的profiles是复数形式,可以设置多个profile名称,并以逗号分隔,我们可以设置多个彼此不相关的profile。

    使用profile进行测试

    在运行集成测试的时候,Spring提供了@ActiveProfiles注解,我们可以死哦那个它来制定运行测试要激活哪个profile.

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes= {Config.class})
    @ActiveProfiles("dev")
    public class PersistenceTest {
        ...
    }

    条件化创建bean

    Spring的profile机制是根据哪个profile处于激活状态来条件化初始bean的一种体现。Spring4中可以使用一种更为通用的机制来实现条件化bean的定义,在这种机制下,条件完全由自己决定。Spring4使用的是@Conditional注解定义条件化bean。

    @Conditional注解可以用在@Bean注解的方法上。如果给定条件为true,就会创建这个bean,否则的话,这个bean会被忽略。

        @Bean
        @Conditional(MagicConditon.class)
        public MagicBean magicBean() {
            return new MagicBean();
        }

    @conditional注解给定了一个Class,它指明了条件——在这里是MagicCondition(实现了Condition接口)。给定@conditional注解的类可以是任意实现了Condition接口的类型。

    public interface Condition {
        boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    }

    这个接口比较简单,只有一个matches()方法作为实现,如果该方法返回true就会创建提供了@Conditional注解的bean,否则就不会创建.

     matches()方法会得到ConditionContext和AnnotatedTypeMetadata两个入参。ConditionContext接口会有一些方法检查bean的定义或bean的属性,AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有其他注解,或是检查@Bean注解的方法上其他注解的属性.

    在Spring4开始,@profile注解就是根据@Conditional和Condition实现的,我们可以参照它的实现。

    处理自动装配的歧义性

    使用Spring的自动封装来装配bean真的是太方便了,但是,这是有且只有一个bean匹配所需条件时,自动装配才有效。如果有多个bean匹配结果的话,这种歧义性会阻碍Spring自动装配。例如使用@Autowired注解标注Dessert接口

        @Autowired
        private Dessert dessert;

    但是这个接口有三个实现:

    @Component
    public class Cake implements Dessert {}
    
    @Component
    public class Cookies implements Dessert {}
    
    @Component
    public class IceCream implements Dessert {}

    这三个实现类都用@Component注解,当组件扫描的时候,它们都会在Spring上下文中创建bean。然后当Spring试图自动装配Dessert的时候,它没有唯一,无歧义的值。Spring就会抛出NoUniqueBeanDefinitonException。

    其实在使用中,自动装配的歧义性并不常见,因为一般情况是给定的类型只有一个实现类,自动装配可以很好地运行。但是,如果真的出现歧义性,Spring提供了:可以将某一个可选bean设为首选(primary)的bean,或者使用限定(qualifier)来帮助Spring将可选的bean'范围缩小到只有一个bean.

    标示首选bean

    当声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配的歧义性。当出现歧义性的时候会直接使用首选的bean。Spring使用的正是@Primary注解,这个注解能够与@Component组合在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明上。

    @Component
    @Primary
    public class Cake implements Dessert {}

    或者使用的是JavaConfig则如下:

    @Bean
    @Primary
    public Dessert iceCream(){
      return new IceCream();  
    }

    如果使用XML配置bean的话,同样可以实现这样的功能。<bean>元素有一个primary属性来指定首选的bean:

    <bean id="iceCream" class="cn.lynu.IceCream" primary="true"/>

    但是,如果你标记了多个首选bean,那么又会带来新的歧义性,事实上也就没有了首选,Spring也无法正常工作。解决歧义性使用限定符是一种更为强大的机制

    限定自动装配的bean

    使用首选bean只能标示一个优先的选择方案,当首选bean数量超过一个,我们就没有其他方法进一步缩小范围。而且,我们每次使用该类型,都会自动使用首选bean,如果在某处不想使用就不好处理了。使用限定可以缩小可选bean的范围直到只有一个bean满足条件,如果还存在歧义性,那么你还可以继续使用更多的限定来进一步缩小范围。@Qualifier注解就是限定,它可以与@Autowired协同使用,在注入时指定想要注入的是哪个bean。

        @Autowired
        @Qualifier("iceCream")
        private Dessert dessert;

    我们已经知道了使用@Component注解声明的类,默认使用bean的ID是首字母小写的类名。因此@Qualifier("iceCream")指向的正是IceCream类的实例.这种情况是将限定与要注入的bean的名称是紧密耦合的,对类名的任何修改都会影响限定失败。这个时候,我们可以使用自定义的限定。

    自定义的限定

    我们可以自定义的设置限定,而不是依赖beanID作为限定,在这里@Qualifier可以与@Component或@Bean配合使用。

    组件:

    @Component
    @Qualifier("cold")
    public class IceCream implements Dessert {}

    或是在JavaConfig方式中:

        @Bean
        @Qualifier("cold")
        public Dessert iceCream() {
            return new IceCream();
        }

    这样我们在注入的时候就可以使用"cold"这个名称了:

        @Autowired
        @Qualifier("cold")
        private Dessert dessert;

    使用自定义的限定注解

    当一个限定不能解决问题的时候,可以使用多个限定来缩小范围,也就是使用多个@Qualifier,但是在Java中不允许出现两个相同的注解(Java8可以,但也要求该注解本身实现@Repeatable注解,不过,Spring的@Qualifier没有实现),这个时候就需要我们创建一个注解了,该注解使用@Qualifier标注,就直接使用我们自定义的注解即可。

    @Target({ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Cold {}

    表明bean限定的时候就可以用:

    @Component
    @Clod
    public class IceCream implements Dessert {}

    我们在注入的时候直接使用这个@Cold注解即可:

        @Autowired
        @Cold
        private Dessert dessert;

    我们可以自定义多个注解来进一步缩小范围

     bean的作用域

    在默认情况下,Spring应用上下文的bean都是作为单例的形式被创建,不管给定的bean被注入到其他的bean多少次,每次注入的都是同一个实例。但有时我们使用的类是易变的,这个时候使用单例就不太好了,因为对象会被修改。

    Spring提供了多种作用域,可以基于这些作用域创建bean:

    • 单例(singleton):这整个应用中只创建bean的一个实例
    • 原型(Prototype):每次注入的时候都会创建一个新的bean的实例(多例)
    • 会话(Session):在Web开发中,为每一个会话创建一个bean实例
    • 请求(Request):在Web开发中,为每一个请求创建一个bean实例

    需要选择除单例之外的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。

    @Component
    @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class Notepad {}

    使用ConfigurableBeanFactory.SCOPE_PROTOTYPE常量比较安全,当也可以使用字面量的方式:@Scope("prototype")

    如果使用JavaConfig的方式:

    @Bean
    @Scop(value=ConfigurableBeanFacory.SCOPE_PROTOTYPE)
    public Notepad notepad(){
      return Notepad();  
    }

    如果使用XML的方式,可以在<bean>元素的scope属性设置:

    <bean id="notepad" class="cn.lynu.Notepad" scope="prototype"/>

    使用会话或请求作用域的bean

    使用会话或请求作用域,还是用@Scope注解,它的使用方式与原型作用域大致相同:

    @Component
    @Scope(value="session",proxyMode=ScopedProxyMode.TARGET_CLASS)
    public class ShoppingCart {}

    Spring会在Web应用的每一个会话中创建一个ShoppingCart实例,但是在对每一个的会话操作中,这个bean实际相当于单例. 注意这里使用了proxyMode属性,这个属性可以解决将会话或请求bean注入到单例bean中所遇到的问题:例如将ShoppingCart bean注入到单例的StoreService bean

    @Component
    public class StoreService{
        @AutoWired
        private ShoppingCart shoppingCart;    
    }    

    当扫描到StoreService 就会创建这个bean,并试图将ShoppingCart bean注入进去,但是ShoppingCart是会话作用域的,此时不存在,只有例如某个用户登录系统,创建会话之后,这个bean才存在;而且会话会有多个,ShoppingCart实例也会有多个,我们不想注入某个固定的实例,应该是当需要使用ShoppingCart实例恰好是当前会话中的那一个。

    使用proxyMode就会先创建一个ShoppingCart bean的代理,将这个代理给StoreService ,当StoreService 真正使用的时候,代理会调用会话中真正的ShoppingCart bean。需要注意的是:如果ShoppingCart是一个接口,需要使用 ScopedProxyMode.INTERFACES JDK动态代理,但如果ShoppingCart是一个具体类,它必须使用CDLib来做代理,必须将proxyMode属性设置为ScopedProxyMode.TERGET_CLASS.表明要生成代理类的拓展类的方式创建代理。

    在XML中声明作用域代理

    如果使用XML的方式就不能使用@Scope注解及其proxyMode属性了。<bean>元素的scope属性能够设置bean的作用域,但是如何指定代理模式呢?

    <bean id="cart" class="cn.lynu.ShoppingCart" scope="session">
        <aop:scoped-proxy />
    </bean>

    默认情况下,它使用CGLib常见目标类的代理,但是我们也可以将proxy-target-class属性设置为false,就是用JDK的代理

    <bean id="cart" class="cn.lynu.ShoppingCart" scope="session">
        <aop:scoped-proxy proxy-target-class="false" />
    </bean>

    运行时值注入

    有的时候,我们可能会希望避免硬编码,而且想让这些值在运行的时候再确定。为了实现这种功能,Spring提供了两种在运行时求值的方式:

    • 属性占位符
    • Spring表达式语言(SpEL)

    注入外部的值

    在Spring中,处理外部属性最简单的但是就是声明属性源并通过Spring的Environment来检索属性:

    @Configuration
    @ComponentScan
    @PropertySource("classpath:app.properties")
    public class Config {
        
        @Autowired
        Environment environment;
        
        @Bean
        public BlankDisc disc() {
            return new BlankDisc(environment.getProperty("disc.title"), 
                    environment.getProperty("disc.artist"));
        }
    }

    文件 app.properties 就是属性源,使用@propertySource注解声明,这个属性文件会加载到Spring的Environment,我们再注入Environment,就可以通过Environment的方法获得属性了。使用getPropperty()方法及其重载方法时,当属指定的属性不存在时,可以通过重载方法给一个默认值,如果没有默认值就会返回null。如果希望这个属性必须存在,那么可以使用getRequiredProperty()方法,Environment还有一些其他方法。

    直接从Environment中获得属性的值,在JavaConfig的方式中非常方便。Spring还提供了占位符装配属性的方法,这些占位符的值来源于一个属性源。

    使用属性占位符

    Spring中可以使用属性占位符的方式将值插入到Spring bean中,在Spring装配中,占位符的形式为使用"${...}"包裹的属性名称。

    <bean id="agtPeppers" class="cn.lynu.BlankDisc"
        c:_title="${disc.title}"
        c:_artist="${disc.artist}"/>

    这样XML文件中没有任何的硬编码,属性的值都是从属性源中获得。如果我们使用的是组件扫描和自动装配,没有使用XML的话,在这种情况下,我们使用@Value注解:

    @Component
    public class BlankDisc {
        
        private String title;
        private String artist;
    
        public BlankDisc() {}
    
        public BlankDisc(@Value("${disc.title}")String title, 
                @Value("${disc.artist}")String artist) {
            this.title = title;
            this.artist = artist;
        }
        
    }

    最后,为了属性占位符可以使用,我们还需要配置一个PropertySourcePlaceholdConfigurer,它可以根据Spring的Environment及其属性源来解析占位符。

        @Bean
        public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
            return new PropertySourcesPlaceholderConfigurer();
        }

    如果使用XML,需要context名称空间的<context:property-placeholder>元素,这个元素会生成PropertySourcePlaceholdConfigurer bean:

    <context:property-placeholder/>
  • 相关阅读:
    HotSpot算法实现
    垃圾收集器(一)
    Java内存区域
    第53条:接口优先于反射机制
    C# 文本转语音,在语音播放过程中停止语音
    C# an error has occurred while updating the entries.see the log file
    音频播放时出现 Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first. https://goo.gl/xX8pDD
    C# CSV文件读写
    图解 SQL-Server新建作业
    FineUI 布局宽度自适应,后台回调js方法
  • 原文地址:https://www.cnblogs.com/lz2017/p/8973338.html
Copyright © 2020-2023  润新知