spring boot基於Java的容器配置

dust1發表於2019-04-01

spring容器是負責例項化、配置、組裝元件的容器。

容器的配置有很多,常用的是xml、Java註解和Java程式碼。

在spring中Ioc容器相關部分是context和beans中。其中context-support儲存著許多執行緒的容器實現。比如AnnotationConfigApplicationContext或者ClassPathXmlApplicationContext。兩者只有接收的目標不同,前者接收Java類後者接收Xml檔案。但作為spring容器的不同實現殊途同歸。

下面我通過spring文件中的介紹來自己過一遍容器配置,來加深印象。這裡我使用的是springboot。所以有些基於web.xml的配置不會涉及到。

歡迎捉蟲

@Bean和@Configuration

@Configuration註解的類,表示這個類是一個配置類,類似於<beans></beans>或者.xml檔案。

@Bean註解用來說明使用springIoc容器管理一個新物件的例項化、配置和初始化。類似於<bean></bean>,預設情況下,bean名稱就是方法名稱.

例子:

@Configuration
public class Conf {
    
    @Bean
    public HelloService helloService() {
        return new HelloServiceImpl();
    }
    
}
複製程式碼

這種配置方式就類似於xml配置中的

<beans>
    <bean id="helloService" class="com.dust.service.impl.HelloServiceImpl" />
</beans>
複製程式碼

等價於註解配置中的

@Service
public class HelloServiceIMpl implements HelloService {
    
    @Override
    public String hello() {
        return "hello world";
    }
    
}
複製程式碼

使用AnnotationConfigApplicationContext例項化Spring容器

這是在spring3.0加入的功能,除了接收@Configuration註解的類作為輸入類之外還可以接受使用JSR-330後設資料註解的簡單類和@Component類。

當@Configuration註解的類作為輸入時,@Configuration類本身會被註冊為一個bean,在這個類中所有用@Bean註解的方法都會被定義為一個bean。

具體有哪些型別的bean可以方法遍歷列印容器中的bean。

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Conf.class);
        HelloService helloService = context.getBean(HelloService.class);
        String hello = helloService.hello();
        System.out.println(hello);
    }
複製程式碼

該例項的步驟為:

1. 建立AnnotationConfigApplicationContext容器物件,同時將@Configuration註解的Conf.class作為引數傳入。
2. 容器回根據傳入的Conf類來構建bean。其中就有helloService
3. 通過bean的物件型別獲取到容器中保管的物件。
4. 執行物件方法
複製程式碼

但是AnnotationConfigApplicationContext並不僅使用@Configuration類。任何@Component或JSR-330註解的類都可以作為輸入提供給建構函式。例如:

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(HelloServiceImpl.class, A.class, B.class);
        HelloService helloService = context.getBean(HelloService.class);
        String hello = helloService.hello();
        System.out.println(hello);
    }
複製程式碼

上面假設MyServiceImpl、A和B都用了Spring的依賴注入的註解,例如@Autowired。

使用register(Class<?>…)的方式構建容器

也可以使用無參建構函式例項化AnnotationConfigApplicationContext,然後使用register()方法配置。當使用程式設計方式構建AnnotationConfigApplicationContext時,這種方法特別有用。

例子:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        HelloService helloService = context.getBean(HelloService.class);
        String hello = helloService.hello();
        System.out.println(hello);
    }
複製程式碼

其中的refresh方法是一個初始化工作。否則註冊的類並不會被生成bean。

使用scan(String …)元件掃描

元件掃描,只需要設定好對應包路徑,spring容器回自動掃描包下面所有能夠被容器初始化的Java類。

使用註解:

@Configuration
@ComponentScan("com.example.springdemo.beans")
public class Conf {

    @Bean
    public HelloService helloService() {
        //用這種方法建立的service相當於用@Service註解標註
        return new HelloServiceImpl();
    }

}
複製程式碼

在該路徑下還有一個配置檔案:

@Configuration
public class Conf2 {

    @Bean
    public ByeService byeService() {
        //用這種方法建立的service相當於用@Service註解標註
        return new ByeServiceImpl();
    }

}
複製程式碼

然後是初始化容器:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        ByeService byeService = context.getBean(ByeService.class);
        String hello = byeService.bye();
        System.out.println(hello);
    }

複製程式碼

可以看到,雖然傳入的是Conf類,但是由於包掃描機制,該容器同時建立了Conf2類中的bean。

這就類似xml配置中的:

<beans>
    <context:component-scan base-package="com.example.springdemo.beans"/>
</beans>
複製程式碼

還可以直接呼叫容器的掃描方法

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//        context.register(Conf.class);
        context.scan("com.example.springdemo.beans");
        context.refresh();
        ByeService byeService = context.getBean(ByeService.class);
        String hello = byeService.bye();
        System.out.println(hello);
    }
複製程式碼

springboot中的包掃描

springboot通過main方法啟動,其中的註解為@SpringBootApplication。通過檢視該註解的程式碼可以發現一下程式碼段:

@AliasFor(
    annotation = ComponentScan.class,
    attribute = "basePackages"
)
複製程式碼

由此可以知道@SpringBootApplication註解包括了包掃描註解,同時掃描的是該類的目錄以及子目錄的所有可以被spring容器初始化的類

AnnotationConfigWebApplicationContext對於web應用的支援

AnnotationConfigApplicationContext在WebApplicationContext中的變體為 AnnotationConfigWebApplicationContext。當配置Spring ContextLoaderListener servlet 監聽器、Spring MVC DispatcherServlet的時候,可以用此實現。

Bean依賴

@Bean註解方法可以具有描述構建該bean所需依賴關係的任意數量的引數。依賴的必須也是Ioc容器中註冊的bean。

將上面的程式碼修改後如下:

@Configuration
public class Conf {

    @Bean
    public HelloService helloService(ByeService byeService) {
        return new HelloServiceImpl(byeService);
    }

    @Bean
    public ByeService byeService() {
        return new ByeServiceImpl();
    }

}
複製程式碼
public class HelloServiceImpl implements HelloService {

    private ByeService byeService;

    public HelloServiceImpl(ByeService byeService) {
        this.byeService = byeService;
    }
    
    @Override
    public String hello() {
        return "hello world" + byeService.bye();
    }
    
}
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        HelloService helloService = context.getBean(HelloService.class);
        String hello = helloService.hello();
        System.out.println(hello);
        ByeService byeService = context.getBean(ByeService.class);
        String bye = byeService.bye();
        System.out.println(bye);
    }
複製程式碼

輸出結果:

hello worldGoodbye!

Goodbye!

這種解決原理和基於建構函式的依賴注入幾乎相同。

生命週期回撥

@Bean註解支援任意的初始化和銷燬回撥方法,這與Spring XML 中bean元素上的init方法和destroy-method屬性非常相似:

    @Bean(initMethod = "init")
    public HelloService helloService(ByeService byeService) {
        return new HelloServiceImpl(byeService);
    }

    @Bean(destroyMethod = "destroy")
    public ByeService byeService() {
        return new ByeServiceImpl();
    }
複製程式碼
public interface ByeService {

    String bye();

    void destroy();

}
複製程式碼
public interface HelloService {

    String hello();

    void init();
}
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        context.close();
    }
複製程式碼

輸出如下:

init helloService!!

destroy byeService!

預設情況下,Ioc容器關閉後所有bean都會被銷燬,但是如果要引入一個生命週期在應用程式之外進行管理的元件,例如:DataSource。那麼只需要將@Bean(destroyMethod =””)新增到你的bean定義中即可禁用預設(推測)模式。

@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}
複製程式碼

當然,初始化的時候也可以先執行對應方法,而不用交給Ioc容器

    @Bean
    public HelloService helloService(ByeService byeService) {
        HelloService helloService = new HelloServiceImpl(byeService);
        helloService.init();
        return helloService;
    }
複製程式碼

@Scope和scope 代理

Scope描述的是Spring容器如何新建Bean例項的。

  1. Singleton:一個Spring容器中只有一個Bean的例項,此為Spring的預設配置,全容器共享一個例項。
  2. Prototype:每次呼叫新建一個Bean例項。
  3. Request:Web專案中,給每一個 http request 新建一個Bean例項。
  4. Session:Web專案中,給每一個 http session 新建一個Bean例項。
  5. GlobalSession:這個只在portal應用中有用,給每一個 global http session 新建一個Bean例項。
    @Bean
    //每次呼叫就建立一個新的bean
    @Scope("prototype")
    public UserInfo userInfo() {
        return new UserInfo();
    }

    @Bean
    public UserService userService() {
        UserService userService = new UserServiceImpl();
        userService.init(userInfo());
        return userService;
    }
複製程式碼

測試程式碼:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        UserService userService = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        UserInfo userInfo = context.getBean(UserInfo.class);
        UserInfo userInfo2 = context.getBean(UserInfo.class);
        System.out.println(userService == userService2);
        System.out.println(userInfo == userInfo2);
    }
複製程式碼

輸出:

true

false

自定義Bean命名

通常,bean的名稱是bean的方法名,但是可以通過name屬性重新命名。有時一個單一的bean需要給出多個名稱,稱為bean別名。為了實現這個目標,@Bean註解的name屬性接受一個String陣列。

    @Bean(name = {"user", "userService", "User"})
    public UserService userService() {
        UserService userService = new UserServiceImpl();
        userService.init(userInfo());
        return userService;
    }
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        Object user = context.getBean("user");
        Object userService = context.getBean("userService");
        Object User = context.getBean("User");

        System.out.println(user == userService);
        System.out.println(user == User);
        System.out.println(userService == User);
    }
複製程式碼

輸出:

true

true

true

Bean描述

有時候需要提供一個詳細的bean描述文字是非常有用的。當對bean暴露(可能通過JMX)進行監控使,特別有用。可以使用@Description註解對Bean新增描述:

    @Bean(name = {"user", "userService", "User"})
    @Description("這是使用者服務物件")
    public UserService userService() {
        UserService userService = new UserServiceImpl();
        userService.init(userInfo());
        return userService;
    }
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        String description = context.getBeanDefinition("user").getDescription();
        System.out.println(description);
    }
複製程式碼

輸出:

這是使用者服務物件

基於Java組合配置

使用@Import註解

和Spring XML檔案中使用元素來幫助模組化配置類似,@Import註解允許從另一個配置類載入@Bean定義:

@Configuration
@Import(UserConf.class)
public class Conf {

    @Bean(initMethod = "init")
    public HelloService helloService(ByeService byeService) {
        //用這種方法建立的service相當於用@Service註解標註
        return new HelloServiceImpl(byeService);
    }

    @Bean(destroyMethod = "destroy")
    public ByeService byeService() {
        return new ByeServiceImpl();
    }

}
複製程式碼
@Configuration
public class UserConf {

    @Bean
    //每次呼叫就建立一個新的bean
    @Scope("prototype")
    public UserInfo userInfo() {
        return new UserInfo();
    }

    @Bean(name = {"user", "userService", "User"})
    @Description("這是使用者服務物件")
    public UserService userService() {
        UserService userService = new UserServiceImpl();
        userService.init(userInfo());
        return userService;
    }

}
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Conf.class);
        context.refresh();
        String description = context.getBeanDefinition("user").getDescription();
        System.out.println(description);
    }
複製程式碼

這種方法簡化了容器例項化,因為只需要處理一個類,而不是需要開發人員在構建期間記住大量的@Configuration註解類。

Java and XML 混合配置

Java配置並不能100%替代xml配置,因此Ioc容器支援兩者混合配置。不過這裡有個區別就是以xml為中心還是以Java配置為中心。

以XML為中心

@Configuration
public class DataSourceConf {

    @Autowired
    private DataSource dataSource;

    @Bean
    public DataSourceService dataSource() {
        return new DataSourceerviceImpl(dataSource);
    }

}
複製程式碼
jdbc.url=jdbc:mysql://39.108.119.174:3306/dust
jdbc.username=root
jdbc.password=123456
複製程式碼
<beans>

    <context:annotation-config/>

    <context:property-placeholder location="classpath:jdbc.properties"/>

    <bean class="com.example.springdemo.beans.DataSourceConf"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

</beans>
複製程式碼
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/datasource.xml");
        DataSourceService dataSourceService = context.getBean(DataSourceService.class);
        System.out.println(dataSourceService.toString());
    }
複製程式碼

以Java類為中心

<beans>
    <context:property-placeholder location="classpath:jdbc.properties"/>
</beans>
複製程式碼
@Configuration
@ImportResource("classpath:spring/datasource.xml")
public class DataSourceConf {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSourceService dataSource() {
        return new DataSourceerviceImpl(url, username, password);
    }

}
複製程式碼
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.scan("com.example.springdemo.beans");
        context.refresh();
        DataSourceService dataSourceService = context.getBean(DataSourceService.class);
        System.out.println(dataSourceService.toString());

//        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/datasource.xml");
//        DataSourceService dataSourceService = context.getBean(DataSourceService.class);
//        System.out.println(dataSourceService.toString());
    }
複製程式碼

相關文章