1. 程式人生 > >如何搭建一個基於Java Config零配置的SSM框架(無配置檔案)

如何搭建一個基於Java Config零配置的SSM框架(無配置檔案)

基於Java形式的專案配置,相比於基於配置檔案的形式更直接,更簡潔,更簡單。使用配置檔案,比如xml,json,properties等形式,都是用程式碼去解析配置檔案內的資訊,然後根據其資訊設定相應配置類的屬性。而Java形式的配置是跳過配置檔案,直接將配置資訊賦值到相應的配置類裡。

以檔案形式配置,一般相應的解析類和配置類都不容易找到,感覺這裡面是一個黑箱,只知道應該這樣配置,但是不清楚為什麼這樣配置,配置是如何發生作用的等等。而以Java形式配置,我們直接和配置類打交道,中間省去了配置檔案和解析類,能很明瞭的看到我們的配置的作用流程,出現問題能更方面的定位及解決。

在常見的Java web專案中,我們通常將專案的啟動資訊配置在web.xml內,比如日誌檔案定位,日誌使用類,Spring啟動類,Spring配置檔案,Filter,SpringMVC配置檔案,MVC根路徑等等。當我們配置好了之後,就能啟動專案了。那麼有沒有能替代web.xml的一個Java類,我們把配置資訊都配置在這類裡面。或者換條路,原先web.xml的配置資訊最終都配置到了哪個或者哪些類,我們能不能直接使用那些配置類,用以配置專案。其實是有的。

原先我們將配置資訊放在web.xml內,以tomcat為例,在啟動時會載入web.xml,解析內部的dom元素,得到我們配置的資訊,然後根據資訊比如Spring啟動類,配置檔案地址來啟動Spring容器。而在Servlet3.0之後,有了一個替代web.xml的java類出現了,叫ServletContainerInitializer,tomcat在啟動後會搜尋專案ServletContainerInitializer介面的指定實現類, 呼叫他們的onStartup方法,而且將Servlet上下文當做引數傳進去。Spring在3.1版本之後新增了一個SpringServletContainerInitializer,實現了ServletContainerInitializer介面,裡面含有啟動整個Spring及SpringMVC的配置選項。Sring在某處配置了這個類的全限定名,tomcat在啟動後會載入此類,在載入的過程中呼叫其onStartup方法,啟動Spring。

推薦Spring版本至少為4,因為Spring4之後新增了很多基於註解形式的配置,用註解加類配置,實在是太好用了。

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

SpringServletContainerInitializer 類上有一個@HandlesTypes,值是WebApplicationInitializer.class,tomcat在啟動時,搜尋org.springframework.web.SpringServletContainerInitializer這個類,解析器上的@HandlesTypes註解的value,按型別獲取專案內所有的實現類,然後將這些實現類和Servlet上下文單做引數傳入onStartup()方法,然後執行此方法。

這裡寫圖片描述

這個類結構圖使用了模板方法模式,留下了很多可以重寫的方法,可以通過重寫這些方法,返回我們的專案配置資訊。通過繼承AbstractAnnotationConfigDispatcherServletInitializer,重寫裡面的方法來自定義配置。下面就講講能重寫哪些方法,每個方法返回的配置的意義。

當我們定義了一個類去繼承AbstractAnnotationConfigDispatcherServletInitializer,看如下程式碼:

public class WebContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootContextConfig.class};
    }

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

    @Override
    protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
        return new ApplicationContextInitializer<?>[] {new RootContextInitializer()};
    }

    @Override
    protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
        return new ApplicationContextInitializer<?>[] {new ServletContextInitializer()};
    }

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

    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        return new Filter[] {characterEncodingFilter};
    }

}

此時,有3個方法是必須實現的,一個返回Spring容器配置類,一個返回SpringMVC容器的配置類,一個返回Servlet根路徑,也就是接受Http請求的根路徑。有很多配置是有預設值的,比如Servlet名稱,是否支援非同步等,我們也可以通過重寫相應方法來自定義配置,感興趣的可以自行檢視這幾個類的內部邏輯。當這幾個類的最頂層的AbstractContextLoaderInitializer的onStartup方法執行時,我們的所有配置均能發生作用,註冊了Spring容器和SpringMVC容器,當然還未初始化。

當onStartup方法執行後,我們向ServletContext內註冊了一個Listener,ContextLoaderListener,將Spring容器配置類設定到了Listener的屬性中,tomcat過後會啟動整個Listener,執行其contextInitialized方法,方法內初始化Spring容器;向ServletContext內註冊了一個Servlet,FrameworkServlet,將SpringMVC容器的配置類設定到其屬性中,在ServletContext執行完Listener的contextInitialized方法後執行Servlet的init方法,在此方法中初始化SpringMVC容器

當然以上只是配置ssm整合框架的啟動類,如何配置Spring,SpringMVC,Mybatis是我們接下來要講的。

第一部分:配置Spring

我們先看Spring容器的配置檔案:

// @ImportResource(locations = {"classpath:com/bob/config/servlet-config.xml"})
// @PropertySource(value = "classpath:com/bob/config/log/log4j.properties", ignoreResourceNotFound =true)
/*@ComponentScan(basePackages = { "com.bob.config.root" }, excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = { UserEnv.class }),
        @Filter(type = FilterType.REGEX, pattern = { "com.bob.config.root" }) })*/
@Configuration
@EnableAsync
@ComponentScan(basePackages = "com.bob.config.root")
@Import({ DataAccessContextConfig.class, RedisCacheContextConfig.class })
@ImportedBeanRegistry(value = "lanboal", telephone = "18758107760")
public class RootContextConfig {

    @Bean
    public ConversionService conversionService() {
        GenericConversionService conversionService = new GenericConversionService();
        conversionService.addConverter(new String2DateConverter());
        return conversionService;
    }

    /* 配置執行緒池
     *
     * {@linkplain ThreadPoolExecutor#execute(Runnable)}
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 執行緒池維護執行緒的最少數量
        executor.setMaxPoolSize(20); // 執行緒池維護執行緒的最大數量
        executor.setKeepAliveSeconds(300); // 空閒執行緒的最長保留時間,超過此時間空閒執行緒會被回收
        executor.setQueueCapacity(30); // 執行緒池所使用的緩衝佇列
        executor.setThreadNamePrefix("Spring-ThreadPool#");
        // rejection-policy:當執行緒池執行緒已達到最大值且任務佇列也滿了的情況下,如何處理新任務
        // CALLER_RUNS:這個策略重試添加當前的任務,他會自動重複呼叫 execute() 方法,直到成功
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    // @Bean,僅僅是為了演示方法可帶引數,Spring解析是會按照@Autowired的形式將合適的Bean注入此引數,然後執行此方法
    public MethodInvokingJobDetailFactoryBean taskInfo(QuartzTaskExample quartzTaskExample) {
        MethodInvokingJobDetailFactoryBean taskInfo = new MethodInvokingJobDetailFactoryBean();
        taskInfo.setTargetObject(quartzTaskExample);
        taskInfo.setTargetMethod("execute");
        return taskInfo;
    }

}

在Spring容器配置類上,最好標識一個@Configuration註解,來指明這是一個配置類,不標也不會有太多問題。最重要的是@ComponentScan,要掃描Spring容器內的相關Bean,但是最好不要掃描到和Http請求相關的Bean,比如Controller等,那些最好放在SpringMVC容器中,同時在Spring容器內的這些Bean,不要飲用到那些屬於SpringMVC容器中的Bean,否則會報錯,因為此時SpringMVC容器還未初始化,那些Bean還不存在。可以用@ImportResource來引入容器配置檔案,可以用@PropertySource來引入properties檔案,可以用@Bean標識方法的形式來定義Bean,方法的返回值就是Bean的型別,方法名稱就是Bean的名稱,和通過掃描得到的Bean同樣效果。還可以通過@Import來引入Bean,我們的Mybatis就是通過此形式引入的。

第二部分:配置Mybatis

我們在Spring容器配置類上通過@Import的形式引入了Mybatis的配置類,也就是說Mybatis的相關Bean最終存放在Spring容器中。

@Configuration
@MapperScan("com.bob.mvc.mapper")
@EnableTransactionManagement(proxyTargetClass = true)
public class DataAccessContextConfig {

    private static final String driverClassName = "com.mysql.jdbc.Driver";
    private static final String username = "root";
    private static final String password = "lanboal";
    // MySQL的JDBC URL編寫方式:jdbc:mysql://主機名稱:連線埠/資料庫的名稱?引數=值
    // 避免中文亂碼要指定useUnicode和characterEncoding
    // 執行資料庫操作之前要在資料庫管理系統上建立一個數據庫,名字自己定,
    // 下面語句之前就要先建立project資料庫
    private static final String url = "jdbc:mysql://localhost:3306/project?useUnicode=true&characterEncoding=UTF8&useSSL=false";

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.addConnectionProperty("useInformationSchema","true"); //針對mysql獲取欄位註釋
        //dataSource.addConnectionProperty("remarksReporting","true");  針對oracle獲取欄位註釋
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setMaxTotal(50);
        dataSource.setMinIdle(5);
        dataSource.setMaxIdle(10);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager txManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 配置MapperConfig
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();

        //當資料庫叢集時,配置多個數據源,通過設定不同的DatebaseId來區分資料來源,同時sql語句中通過DatabaseId來指定匹配哪個資料來源
        //configuration.setDatabaseId("Mysql-1");

        //設定Mybatis的SQL執行器的使用方式: 執行器重用
        //configuration.setDefaultExecutorType(ExecutorType.REUSE);

        // 這個配置使全域性的對映器啟用或禁用快取
        configuration.setCacheEnabled(true);
        // 允許 JDBC 支援生成的鍵,需要適合的驅動(如MySQL,SQL Server,Sybase ASE)。
        // 如果設定為 true 則這個設定強制生成的鍵被使用,儘管一些驅動拒絕相容但仍然有效(比如 Derby)。
        // 但是在 Oracle 中一般不需要它,而且容易帶來其它問題,比如對建立同義詞DBLINK表插入時發生以下錯誤:
        // "ORA-22816: unsupported feature with RETURNING clause" 在 Oracle
        // 中應明確使用 selectKey 方法
        configuration.setUseGeneratedKeys(false);
        // 配置預設的執行器。SIMPLE 執行器沒有什麼特別之處;REUSE 執行器重用預處理語句;BATCH 執行器重用語句和批量更新
        configuration.setDefaultExecutorType(ExecutorType.REUSE);
        // 全域性啟用或禁用延遲載入,禁用時所有關聯物件都會即時載入
        configuration.setLazyLoadingEnabled(false);
        // 設定SQL語句執行超時時間預設值,具體SQL語句仍可以單獨設定
        configuration.setDefaultStatementTimeout(5000);
        sqlSessionFactoryBean.setConfiguration(configuration);
        // 匹配多個 MapperConfig.xml, 使用mappingLocation屬性
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:com/bob/**/*Mapper.xml"));
        return sqlSessionFactoryBean.getObject();
    }
}

通過@MapperScan指明要掃描的Mapper介面;通過@EnableTransactionManagement的形式宣告支援事務;通過@Bean的形式定義資料來源,事務管理和sqlSessionFactory。在sqlSessionFactory指定需要載入的sql xml檔案。mybatis配置檔案載入時就會針對那些mapper介面使用cglib生成代理實現類。

第三部分:配置SpringMVC

配置SpringMVC框架有一個很重要的註解,@EnableWebMvc,他的作用類似配置檔案形式的

<mvc:annotation-griven />

幫我們配置了SpringMVC內很多重要的元件。@EnableWebMvc註解上有@Import註解,引入了DelegatingWebMvcConfiguration,其父類是WebMvcConfigurationSupport,在這類裡有很多@Bean標識的方法, 在解析時會生成很多MVC框架內的元件,省了我們很多的功夫。

同時有一個很重要的配置類WebMvcConfigurerAdapter,我們繼承這個類,重寫那些想要配置的方法。

@Configuration
@EnableAsync
@EnableWebMvc
@ComponentScan(basePackages = {"com.bob.mvc"}, basePackageClasses = {MvcContextConfig.class}, excludeFilters = {
    @Filter(type = FilterType.CUSTOM, classes = {MvcContextScanExcludeFilter.class})})
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Import({AppUserContextConfig.class, TimerContextConfig.class, KafkaContextConfig.class})
public class MvcContextConfig extends WebMvcConfigurerAdapter {

    final static Logger LOGGER = LoggerFactory.getLogger(MvcContextConfig.class);

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    /**
     * 定義檔案上傳的處理器
     *
     * @return
     */
    @Bean("multipartResolver")
    public CommonsMultipartResolver commonsMultipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(10 * 1024 * 1024);
        return multipartResolver;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        stringConverter.setWriteAcceptCharset(false);
        converters.add(stringConverter);
        converters.add(new ByteArrayHttpMessageConverter());
        converters.add(new ResourceHttpMessageConverter());
        converters.add(new MappingJackson2HttpMessageConverter());
        converters.add(new MappingJackson2XmlHttpMessageConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.useJaf(false).favorPathExtension(false).favorParameter(true).parameterName("mediaType")
            .ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods("GET", "POST", "DELETE", "PUT").maxAge(3600);

    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LoginInterceptor intcep = new LoginInterceptor();
        registry.addInterceptor(new MappedInterceptor(intcep.getIncludePatterns(), intcep.getExcludePatterns(), intcep));
    }

    @Override
    public Validator getValidator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setProviderClass(HibernateValidator.class);
        ReloadableResourceBundleMessageSource messageResource = new ReloadableResourceBundleMessageSource();
        messageResource.setBasenames("classpath:com/bob/validation/ValidationMessages");
        validator.setValidationMessageSource(messageResource);
        return validator;
    }

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        exceptionResolvers.add(new CustomizedExceptionResolver());
    }

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new StudentFormatter());
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setDefaultTimeout(5 * 1000);
        configurer.setTaskExecutor(threadPoolTaskExecutor);
        configurer.registerCallableInterceptors(new AsyncCallableInterceptor());
        configurer.registerDeferredResultInterceptors(new AsyncDeferredResultInterceptor());
    }

}

我們通過重寫WebMvcConfigurerAdapter 的方法做的配置,在WebMvcConfigurationSupport的@Bean的方法執行時會用上,感興趣可以自行檢視@Bean方法的執行邏輯。

Spring容器在初始化後,會被設定為SpringMVC容器的父容器,也就是說SpringMVC內可以獲取Spring容器的Bean,Spring容器不能獲取SpringMVC容器內的Bean。

當然WebMvcConfigurerAdapter 還有很多可以重寫的方法, 可以做很多其他的配置,感興趣的可以自行檢視其程式碼。每個配置的含義可以百度,都很好理解的。

經過以上步驟,java形式的ssm框架整合就已經完成了,很多其他的框架也可以使用java形式來配置,比如kafka,可以使用@EnableKafka的形式來引入,schedule可以使用@EnableScheduling引入等等。使用java形式來配置是一個大勢,希望這篇部落格能對你有所啟發。