spring5入門與實踐第三講Spring的其他特性

weixin_34321977發表於2018-03-20

spring整合測試

spring可以和junit很好的進行整合,首先我們需要新增junit和spring和test的依賴

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.2.RELEASE</version>
    <scope>test</scope>
</dependency>

建立一個配置類

@Configuration
public class BaseConfig {
    @Bean
    public String hello() {
        return "hello";
    }
}

建立基於spring 的測試類,首先在測試類上新增@RunWith(SpringJUnit4ClassRunner.class),說明這個類是一個spring的測試類,宣告之後該測試類就可以直接注入spring容器中的物件,之後通過@ContextConfiguration(classes=BaseConfig.class)來說明具體的配置配置類是哪個。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=BaseConfig.class)
public class BaseTest {
    @Autowired
    private String hello;
    @Test
    public void test01() {
        System.out.println(hello);
    }
}

在BaseConfig中建立了一個String的Bean物件,使用的是hello方法建立的,此時注入物件名稱就是hello,所以在BaseTest中直接注入一個hello的物件即可。

spring基於Annotation的配置

spring4之後,spring就支援完全基於java的Annotation的配置,這裡需要特別拿出來講解幾個比較常用的方法,首先是如何分割多個配置檔案,通過import既可以完成分割,建立兩個配置類

@Configuration
public class AConfig {

    @Bean("a")
    public String a() {
        return "a";
    }
}

@Configuration
public class BConfig {
    @Bean
    public String b() {
        return "b";
    }
}

在BaseConfig中通過@Import可以匯入這些配置類

@Configuration
@Import({AConfig.class,BConfig.class})
public class BaseConfig {
    @Bean("hello")
    public String hello() {
        return "hello";
    }
}

在測試類中只要引入BaseConfig,其他兩個測試類也會被匯入

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=BaseConfig.class)
public class BaseTest {
    @Autowired
    private String hello;

    @Autowired
    private String a;

    @Autowired
    private String b;

    @Test
    public void test01() {
        System.out.println(hello);
        System.out.println(a);
        System.out.println(b);
    }
}

在@Bean中除了預設的value之外,還提供了initMethod和destoryMethod來執行初始化和銷燬操作,建立一個BaseObject來執行

public class BaseObject {
    private void init() {
        System.out.println("begin run....");
    }
    private void destory() {
        System.out.println("destory program!!");
    }
}

在Config類中加入該bean

@Bean(initMethod = "init",destroyMethod = "destory")
public BaseObject baseObject() {
    return new BaseObject();
}

此時只要注入這個物件就會首先執行init和destroy方法。

spring的Profile

spring從3之後就加入了Profile的處理,Profile可以分階段和分使用者來設定配置,該功能在多使用者管理中特別的好用,我們從java的配置資訊和properties的資訊讀取兩方面來進行處理。

假設有這樣一種需要,專案中需要設定靜態資原始檔的路徑,首先建立一個SystemPath的介面和兩個不同的實現類,一個用來指定開發時的路徑,一個用來指定釋出後的路徑

public interface SystemPath {
    String getRealPath();
}

public class DevPath implements SystemPath {
    public String getRealPath() {
        return "dev:path";
    }
}

public class QaPath implements SystemPath {
    public String getRealPath() {
        return "qa:path";
    }
}

下一步在具體的配置類中建立這兩個bean並且指定Profile

@Configuration
@Import({AConfig.class,BConfig.class})
public class BaseConfig {
    ....    
    @Bean("path")
    @Profile("dev")
    public SystemPath devPath() {
        return new DevPath();
    }
    
    @Bean("path")
    @Profile("qa")
    public SystemPath qaPath() {
        return new QaPath();
    }

}

最後就是在使用的時候啟用,在基於web的應用的程式中,可以通過web.xml來進行設定,通過spring.profiles.active和spring.profiles.default來配合使用,spring.profiles.active用來指定當前啟用的配置,如果沒有設定spring.profiles.active這個的值,會自動去找spring.profiles.default,可以在web.xml中通過<context>來進行設定。此處會在web專案時介紹。

如果需要在maven中的使用Profile,可以將這兩個引數加在maven的命令之後,如果希望使用在測試類中,使用@ActiveProfiles來進行啟用

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BaseConfig.class)
@ActiveProfiles("dev")
public class PathTest {
    @Autowired
    private SystemPath path;

    @Test
    public void testPath() {
        System.out.println(path.getRealPath());
    }
}

以上就是Profile的思路,在實際應用中,這種需求更多的是運用在properties的環境中,當需要多個使用者同時開發時難免檔案的路徑,資料庫的使用者名稱和密碼這些有不同,如果同時使用svn或者git這些版本管理工具,只有一個properties檔案會很難處理,所以可以根據使用者建立多個不同的配置檔案,根據不同的需求進行載入,下面將介紹基於不同配置檔案的實現方式。

首先建立三個配置檔案application.properties,application-kh.properties,application-ls.properties

#### application.properties
profile.name = default.profile
realpath = /project/test
jdbc.username = ok
jdbc.password = 111111

#### application-kh.properties
profile.name = kh.profile
realpath = d:/project/test
jdbc.username = kh
jdbc.password = 123456

#### application-ls.properties
profile.name = ls.profile
realpath = d:/project/test
jdbc.username = ls
jdbc.password = 666666

建立兩個類,一個類模擬資料庫的配置,一個類模擬環境的配置

@Component
public class DataSourceProperties {
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    ..省略getter和setter...
}

@Component
public class EnvProperties {
    @Value("${profile.name}")
    private String profileName;
    @Value("${realpath}")
    private String realpath;

   ..省略getter和setter...
}


這兩類都是spring的Component,一個儲存了資料庫的基本資訊,一個儲存了環境的資訊,都是通過properties來獲取這兩個值,下面就需要在BaseConfig這個配置類中新增相應的Properties,雖然spring提供了@PropertySource,但是這種方式並不能很好的實現Profile,它只會把每一個properties檔案都載入進去,如果有相同的值會用最後一個來替換,這顯然無法實現基於Profile的Properties。所以此處需要開發人員根據ActiveProfile手動把相應的配置檔案匯入,通過建立一個init的方法來執行

@Configuration
@Import({AConfig.class,BConfig.class})
@ComponentScan("org.konghao")
public class BaseConfig {
    @Autowired
    private ConfigurableEnvironment env;

    private String prefix = "application";

    @PostConstruct//該方法在建構函式之後執行
    public void init() {
        try {
            //判斷環境引數中是否有ActiveProfiles
            if(env.getActiveProfiles().length>0) {
                //如果加了Profile,就將該Profile的字首新增到spring的Property資源中
                for(String activeProfile:env.getActiveProfiles()) {
                    //獲取字首的資原始檔
                    ClassPathResource cpr = new ClassPathResource(prefix+"-"+activeProfile+".properties");
                    if(cpr.exists()) {
                        //新增到環境的資原始檔中
                        env.getPropertySources().addLast(new ResourcePropertySource(cpr));
                    }
                }
            } else {
                //如果沒有Profile,就把預設的檔案新增進去
                ClassPathResource cpr = new ClassPathResource(prefix+".properties");
                if(cpr.exists()) {
                    env.getPropertySources().addLast(new ResourcePropertySource(cpr));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用這種方式就可以有效的解決properties檔案的Profile問題。

條件化Bean

在Spring4之後,spring提供了一種基於條件來建立Bean的方式,這可以用於某種特殊的需求,如只有在某個Bean建立成功了才建立該Bean,或者說只有在配置了某個環境變數之後才建立這個Bean,這些需求通過單純的配置是不太容易實現的。

通過@Conditional 來設定建立該Bean的條件,@Conditional 中要傳入Condition 的物件,所以需要我們手動實現這個物件

public class HelloBeanCheck implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //conditionContext.getBeanFactory()//獲取BeanFactory
//        conditionContext.getRegistry()//獲取Bean的定義物件
//        conditionContext.getBeanFactory()//獲取Bean的工廠
//        conditionContext.getClassLoader()//獲取ClassLoader
//        conditionContext.getEnvironment()//獲取環境變數
//        conditionContext.getResourceLoader()//獲取資源資訊
//        annotatedTypeMetadata可以用來檢測自己的Annotation資訊
        //如果有hello這個bean才會建立HelloBean
        return conditionContext.getRegistry().containsBeanDefinition("hello");
    }
}

Condition中需要實現一個matches方法,該方法非常強大,可以通過該方法兩個引數做很多檢查操作,大家可以自行測試,最後如果返回true就會建立,否則就不會建立。

動態注入值

在原來的程式碼中,基本都是以硬編碼的方法為Bean注入值的,如果希望動態的注入值也是可以實現的,有兩種具體的解決方案,一種是基於配置檔案的方式,另外一種就是基於spring的表示式來實現。

基於配置檔案的實現方式非常簡單,首先通過@PropertySource新增一個資原始檔,其次注入一個Environment物件即可,上一小節所使用的ConfigurableEnvironment是Environment的子類。建立一個User物件,並且建立一個base.properties的資原始檔

user.username = zs
user.nickname = zhangsan

下面建立User物件

public class User {
    private String username;
    private String nickname;

    public User(){}

    public User(String username,String nickname) {
        this.username = username;
        this.nickname = nickname;
    }
    //........省略getter和setter.....
}

在BaseConfig中注入該物件

@Configuration
@Import({AConfig.class,BConfig.class})
@ComponentScan("org.konghao")
@PropertySource("base.properties")
public class BaseConfig {
    @Autowired
    private ConfigurableEnvironment env;

    //...省略多餘程式碼..
        
    @Bean
    public User user() {
        return new User(env.getProperty("user.username"),
                env.getProperty("user.nickname"));
    }
}

如果不使用env,也可以使用${xxx.xxx}的佔位符來指定

public class User {
    @Value("${user.username}")
    private String username;
    @Value("${user.nickname}")
    private String nickname;
}

此時在BaseConfig中直接使用不帶引數的構造方法來建立,都會給username和nickname設定到相應的值

 @Bean
    public User user() {
//        return new User(env.getProperty("user.username"),
//                env.getProperty("user.nickname"));
        return new User();
    }

基於配置的方法非常簡單,下面看看基於spring表示式SpEL的方式。

spring表示式是使用#{...} 來編寫,表示式非常的強大,可以是單個的值,也可以是物件,還能是物件的某個屬性,並且這些值還可以進行運算,spring的SpEL是使用@Value來注入,建立三個類來模擬一下SpEL。

@Component
public class Student {

    @Value("#{systemProperties['user.name']}")
    private String name;
    private List<Book> books;

    public Student() {
        books = Arrays.asList(
                new Book("b1",12),
                new Book("b2",33),
                new Book("b3",44));
    }

   //...省略getter和setter....
}

此處就使用了一個SpEL,從系統變數中獲取使用者名稱,下面看看Book這個類

@Component
public class Book {
    private String name;
    private double price;

    public Book() {}

    public Book(String name,double price) {
        this.name = name;
        this.price = price;
    }

   //...省略getter和setter...
}

通過一個StudentDto來模擬幾種常用的SpringSPEL

@Component
public class StudentDto {
    @Autowired
    private Student student;

    @Value("#{student.name}")
    private String name;

    @Value("#{student.books.![name]}")
    private List<String> books;

    @Value("#{student.books.size()}")
    private int bookCount;

    @Value("#{T(org.konghao.spring.model.BookUtil).calPrice(student.books)}")
    private double bookPrice;

    //...省略來的getter和setter...
}

name這個屬性使用的是Student的name,books屬性使用的是一組books的name列表,bookCount通過list的size求和,而bookPrice是呼叫了BookUtil中 方法求所有書的價格,只要是需要使用某個類的靜態方法或者引用常量都需要使用T()來表示,下面看看BookUtil,

public class BookUtil {
    public static double calPrice(List<Book> books) {
        return books.stream().mapToDouble(s->s.getPrice()).sum();
    }
}

以上使用了Lamdba表示式來完成求和,強烈建議大家將來使用List都通過Lamdba來處理。這一部分的內容就到此為止了,主要講解了spring的一些用法,下一部分進入spring的web程式設計講解。

相關文章