在Spring Web Application中使用Annotation取代XML

總結

  1. 使用WebApplicationInitializer取代web.xml
  2. 使用@Configuration來載入Spring設定

瞭解web.xml中spring的用途

web.xml範例

<?xml version="1.0" encoding="utf-8" ?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <!-- Spring context loader -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 共用的spring config, 如dataSource, DAO , Service -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:/applicationContext.xml</param-value>
    </context-param>

    <!-- Spring Request Dispatcher -->
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

可以看到web.xml裡主要做了三件事

  1. 載入ContextLoaderListener,用來完成下面兩件事
  2. 指出共用的spring component設定檔以便載入
  3. 指出Spring webmvc用的設定檔,以便產生Dispatcher

所以後續要做的事,就是想辦法一步步用annotation完成這三件事,

從xml到annotaion

@Configuration及@Bean

其實不久前,我們才從使用Builder Pattern的純Java程式轉到使用xml設定,沒想到沒多久我們又要轉回到純Java程式來,不過這次多了Annotation,至少可以用比較共通的方式來表達我們的意圖。

先看個範例吧

<?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/spring-beans.xsd">

    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <property name="poolName" value="baseCP" />
        <property name="connectionTestQuery" value="SELECT 1" />
        <property name="driverClassName" value="org.h2.Driver" />
        <property name="jdbcUrl" value="jdbc:h2:tcp://localhost/~/test" />
        <property name="username" value="sa" />
        <property name="password" value="" />

        <property name="maximumPoolSize" value="5" />
    </bean>

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <constructor-arg ref="hikariConfig" />
    </bean>
</beans>

這是個產生共用dataSource的spring xml設定檔,改用annotation就兩件事

  1. 用@Configuration加在Class上,表示這是一個專門做spring設定用的Class,來完成原有xml裡的設定。
  2. 用@Bean加在有回傳物件的Method上,來完成裡所做的事

所以轉成java程式大致就像這樣

@Configuration
public class ApplicationConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setPoolName("baseCP");
        config.setConnectionInitSql("SELECT 1");
        config.setDriverClassName("org.h2.Driver");
        config.setJdbcUrl("jdbc:h2:tcp://localhost/~/test");
        config.setUsername("sa");
        config.setPassword("");
        config.setMaximumPoolSize(5);

        HikariDataSource dataSource = new HikariDataSource(config);
        return dataSource.getDataSource();
    }
}

移除web.xml

自Servlet 3.0後,WEB-INF目錄下可以不用加入web.xml,要做initialize的工作可用下列兩種方式

  1. 使用@WebServlet, @WebFilter, @WebListener來標示原有的servlet,filter,listener
  2. 實作javax.servlet.ServletContainerInitializer,來處理各項工作。

Spring用了第二種方式,在SpringServletContainerInitializer中實作了ServletContainerInitializer,再指定了HandlesTypes。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
            ....
    }
}

所以在Spring Web Application中,只要是WebApplicationInitializer的subclass都會被丟進來處理,所以切入點就很明顯了;只要我們從WebApplicationInitializer下手,大概就能完成使用annotation載入的工作。

可用的WebApplicationInitializer

直接參考Spring doc中的AbstractAnnotationConfigDispatcherServletInitializer,可以看到目前Spring提供的subclass

org.springframework.web.servlet.support
Class AbstractAnnotationConfigDispatcherServletInitializer

java.lang.Object
org.springframework.web.context.AbstractContextLoaderInitializer
org.springframework.web.servlet.support.AbstractDispatcherServletInitializer
org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer
All Implemented Interfaces:
WebApplicationInitializer

WebApplicationInitializer、AbstractContextLoaderInitializer、AbstractDispatcherServletInitializer及AbstractAnnotationConfigDispatcherServletInitializer,這四個class任一個都能完成載入的必要工作,請擇一即可,沒特別需求,不要併用啊…

使用WebApplicationInitializer範例

public class MyWebInitializer implements WebApplicationInitializer {
    private static final Logger logger = LoggerFactory.getLogger(MyWebInitializer.class);

    public void onStartup(ServletContext servletContext) throws ServletException {
        logger.info("----------------------------------");
        logger.info("do startup from MyWebInitializer");
        logger.info("----------------------------------");

        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(ApplicationConfig.class);

        servletContext.addListener(new ContextLoaderListener(applicationContext));

        AnnotationConfigWebApplicationContext dispatchContext = new AnnotationConfigWebApplicationContext();
        dispatchContext.register(WebMvcConfig.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("spring", new DispatcherServlet(dispatchContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

再複習一下web.xml要做的三件事

  1. 載入ContextLoaderListener,用來完成下面兩件事
  2. 指出共用的spring component設定檔以便載入
  3. 指出Spring webmvc用的設定檔,以便產生Dispatcher
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(ApplicationConfig.class);

就是載入共用的spring component

servletContext.addListener(new ContextLoaderListener(applicationContext));

使用ContextLoaderListener

AnnotationConfigWebApplicationContext dispatchContext = new AnnotationConfigWebApplicationContext();
        dispatchContext.register(WebMvcConfig.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("spring", new DispatcherServlet(dispatchContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");

載入Spring webmvc用的設定檔,以便產生Dispatcher

使用AbstractAnnotationConfigDispatcherServletInitializer範例

public class MyWebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    private static final Logger logger = LoggerFactory.getLogger(MyWebConfig.class);

    public MyWebConfig() {
        logger.info("----------------------------------");
        logger.info("do startup from MyWebConfig");
        logger.info("----------------------------------");
    }
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{ApplicationConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebMvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

一樣是完成必要的三件事,只是一堆要自己new的操作就免了,只要指出各類的configuration class即可。

上面這些只是基本,再來Spring MVC要用的adaptor、unit test及build的設定才是要多花時間的部份…

One thought to “在Spring Web Application中使用Annotation取代XML”

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料