如何搭建一個基於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形式來配置是一個大勢,希望這篇部落格能對你有所啟發。