去xml配置也算是一个趋势,在Spring boot上可以轻易做到无xml配置,在Spring mvc中也是可以的。在Spring 3.1之后就支持了,结合servlet3.0,可以简化很多配置。

先来说说这个demoframe项目的简化。首先是参考beetlsql的Springdemo,applicationContext.xml和spring-servlet.xml,把原先spring-servlet.xml中的配置都剪切到applicationContext.xml中,spring-servlet.xml只留下一个空的配置文件。applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:mvc="http://www.springframework.org/schema/mvc"
	   xmlns:tx="http://www.springframework.org/schema/tx"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xmlns:p="http://www.springframework.org/schema/p"
	   xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
		http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<!-- 引入外部配置 -->
	<context:property-placeholder location="classpath:config.properties"/>

	<!-- 把标记了@Controller注解的类转换为bean -->
	<context:component-scan base-package="cn.demoframe" />

	<!-- 在SpringMVC中使用JSON必须配置 -->
	<mvc:annotation-driven>
		<mvc:message-converters register-defaults="true" >
			<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
				<property name="supportedMediaTypes" value="application/json;charset=UTF-8" />
				<property name="fastJsonConfig">
					<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
						<property name="serializerFeatures">
							<array value-type="com.alibaba.fastjson.serializer.SerializerFeature">
								<value>QuoteFieldNames</value>
								<value>WriteMapNullValue</value>
								<value>WriteNullStringAsEmpty</value>
								<value>WriteNullListAsEmpty</value>
							</array>
						</property>
					</bean>
				</property>
			</bean>
		</mvc:message-converters>
	</mvc:annotation-driven>

	<mvc:interceptors>
		<bean class="cn.demoframe.test.filter.SessionInterceptor" />
	</mvc:interceptors>

	<!-- 对模型视图名称的解析,即在模型视图名称添加前后缀 -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
			p:prefix="/view" p:suffix=".jsp" />

	<!-- 定时器 -->
	<!--<import resource="classpath:app-scheduling.xml"/>-->

	<!-- 数据源配置(druid) -->
	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
		  init-method="init" destroy-method="close">
		<property name="url" value="${db.url}" />
		<property name="username" value="${db.username}" />
		<property name="password" value="${db.password}" />
		<property name="maxActive" value="70" />
		<property name="initialSize" value="5" />
		<property name="maxWait" value="60000" />
		<property name="minIdle" value="1" />
		<property name="testWhileIdle" value="true" />
		<property name="validationQuery" value="SELECT 1 FROM DUAL" />
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<property name="testOnBorrow" value="false" />
		<property name="filters" value="config" />
		<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${db.publickey}" />
		<!-- <property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->
	</bean>

	<!-- 日志数据源配置(druid) -->
	<bean id="logDataSource" class="com.alibaba.druid.pool.DruidDataSource"
		  init-method="init" destroy-method="close">
		<property name="url" value="${db.log.url}" />
		<property name="username" value="${db.log.username}" />
		<property name="password" value="${db.log.password}" />
		<property name="maxActive" value="50" />
		<property name="initialSize" value="1" />
		<property name="maxWait" value="60000" />
		<property name="minIdle" value="1" />
		<property name="testWhileIdle" value="true" />
		<property name="validationQuery" value="SELECT 1 FROM DUAL" />
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<property name="testOnBorrow" value="false" />
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
		<property name="filters" value="config" />
		<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${db.log.publickey}" />
	</bean>

	<!-- 整合mybatis -->
	<bean id="logSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="logDataSource" />
		<property name="configLocation" value="classpath:SqlMapConfigLog.xml" />
	</bean>

	<!-- 整合mybatis -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:SqlMapConfig.xml" />
	</bean>

	<!-- 配置事务管理 -->
	<bean id="transactionManager"
		  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!-- 定义SERVICE事务通知 -->
	<tx:advice id="txAdvice_service" transaction-manager="transactionManager">
		<!-- 定义方法的过滤规则 -->
		<tx:attributes>
			<!-- 所有方法都使用事务 -->
			<tx:method name="*" propagation="REQUIRED"/>
			<!-- 定义所有get开头的方法都是只读的 -->
			<tx:method name="get*" read-only="true"/>
			<!-- 或参考另一种规则设置方式  -->
			<!-- <tx:method name="add*" /> -->
			<!-- <tx:method name="update*" /> -->
			<!-- <tx:method name="delete*" /> -->
			<!-- <tx:method name="*" read-only="true" /> -->
		</tx:attributes>
	</tx:advice>

	<!-- 定义AOP配置 -->
	<aop:config proxy-target-class="true">
		<!-- 适配切入点(service)和事务的通知 -->
		<aop:advisor pointcut="execution(* *..service..impl..*.*tx(..))"
					 advice-ref="txAdvice_service" />
	</aop:config>

	<!-- redis 配置 -->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxIdle" value="300" />
		<property name="maxTotal" value="600" />
		<property name="maxWaitMillis" value="10000" />
		<property name="testOnBorrow" value="true" />
	</bean>

	<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<property name="usePool" value="true" />
		<property name="hostName" value="${redis.host}" />
		<property name="port" value="${redis.port}" />
		<property name="password" value="${redis.pwd}" />
		<property name="timeout" value="100000" />
		<property name="database" value="0" />
		<constructor-arg index="0" ref="jedisPoolConfig" />
	</bean>

	<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
		<property name="connectionFactory" ref="connectionFactory" />
	</bean>

	<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
		<constructor-arg index="0">
			<set>
				<bean class="redis.clients.jedis.HostAndPort">
					<constructor-arg index="0" value="${redis.host1}"/>
					<constructor-arg index="1" value="${redis.port1}"/>
				</bean>
				<bean class="redis.clients.jedis.HostAndPort">
					<constructor-arg index="0" value="${redis.host1}"/>
					<constructor-arg index="1" value="${redis.port2}"/>
				</bean>
				<bean class="redis.clients.jedis.HostAndPort">
					<constructor-arg index="0" value="${redis.host1}"/>
					<constructor-arg index="1" value="${redis.port3}"/>
				</bean>
			</set>
		</constructor-arg>
		<constructor-arg index="1" value="1000"/>
		<constructor-arg index="2" value="1000"/>
		<constructor-arg index="3" ref="jedisPoolConfig"/>
	</bean>

</beans>

spring-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-4.2.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

</beans>

web.xml不用改动,正常运行。
之后是第一步改造,去web.xml,经过多次调查,暂时没找到合适的方案替代<welcome-file-list>这个标签,还有就是<session-config>的<session-timeout>,暂时就留着web.xml这个文件,其他跟Spring相关的配置都已经移到Java代码中了。

第一步暂时保留applicationContext.xml和spring-servlet.xml这两文件。

public class WebAppInitializer implements WebApplicationInitializer {

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {

		servletContext.addListener(new ContextLoaderListener());

		servletContext.setInitParameter("contextConfigLocation", "classpath:applicationContext.xml");

		servletContext.setInitParameter("logbackConfigLocation", "classpath:logback.xml");

		ServletRegistration.Dynamic servlet = servletContext.addServlet("spring", new DispatcherServlet(ctx));
		servlet.addMapping("*.do");
		servlet.setLoadOnStartup(1);
		servlet.setInitParameter("contextConfigLocation", "classpath:spring-servlet.xml");

		FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("Set Character Encoding", new CharacterEncodingFilter("UTF-8"));
		encodingFilter.addMappingForUrlPatterns(null, true, "/");
	}
}

Spring提供了一个WebApplicationInitializer接口,就是用来启动容器的,直接把相关的配置写到onStartup里面就能初始化整个容器。

初步改造完成之后,就是考虑如何去掉两个xml配置文件。

Spring的相关配置,可以继承WebMvcConfigurerAdapter这个适配器,把相关的配置写进去,这里也是参考网上找到的代码,将自己的部分改造。主要还是设置返回格式的部分,json由fastjson处理。

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MvcConfig extends WebMvcConfigurerAdapter {

	@Override
	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
		configurer.mediaType("json", MediaType.valueOf("application/json"));
		configurer.mediaType("xml",MediaType.valueOf("application/xml"));
		configurer.mediaType("html",MediaType.valueOf("text/html"));
		configurer.mediaType("*",MediaType.valueOf("*/*"));
	}

	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
		List<MediaType> list = new ArrayList<MediaType>();
		list.add(new MediaType("text","plain",Charset.forName("UTF-8")));
		list.add(new MediaType("*","*",Charset.forName("UTF-8")));
		stringConverter.setSupportedMediaTypes(list);

		FastJsonHttpMessageConverter jsonConverter = new FastJsonHttpMessageConverter();
		List<MediaType> jsonList = new ArrayList<MediaType>();
		jsonList.add(MediaType.valueOf("application/json;charset=UTF-8"));
		jsonList.add(MediaType.valueOf("text/plain;charset=utf-8"));
		jsonList.add(MediaType.valueOf("text/html;charset=utf-8"));
		jsonConverter.setSupportedMediaTypes(jsonList);
		FastJsonConfig config = new FastJsonConfig();
		config.setSerializerFeatures(new SerializerFeature[]{SerializerFeature.QuoteFieldNames,
				SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullListAsEmpty});
		jsonConverter.setFastJsonConfig(config);

		converters.add(stringConverter);
		converters.add(jsonConverter);
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new SessionInterceptor());
	}
}

Spring web的主要配置,是可以根据需要写多个配置文件的。主要还是扫描@Configuration这个标签。

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "cn.demoframe")
public class WebConfig {

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

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

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

	@Value("${db.publickey}")
	private String publickey;

	@Bean
	public InternalResourceViewResolver internalResourceViewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/view");
		viewResolver.setSuffix(".jsp");
		return viewResolver;
	}

	@Bean
	public static PropertyPlaceholderConfigurer loadProperties() {
		PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
		ClassPathResource resource = new ClassPathResource("config.properties");
		configurer.setLocations(resource);
		return configurer;
	}

	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		dataSource.setUrl(url);
		dataSource.setUsername(username);
		dataSource.setPassword(password);
		dataSource.setMaxActive(70);
		dataSource.setInitialSize(5);
		dataSource.setMaxWait(60000);
		dataSource.setMinIdle(1);
		dataSource.setTestWhileIdle(true);
		dataSource.setValidationQuery("select 1 from dual");
		dataSource.setTimeBetweenEvictionRunsMillis(60000);
		dataSource.setTestOnBorrow(false);
		try {
			dataSource.setFilters("config");
		} catch (SQLException e) {
			e.printStackTrace();
		}
		Properties properties = new Properties();
		properties.setProperty("config.decrypt", "true");
		properties.setProperty("config.decrypt.key", publickey);
		dataSource.setConnectProperties(properties);
		return dataSource;
	}

	@Bean
	public SqlSessionFactoryBean sqlSessionFactory() {
		SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
		sqlSessionFactory.setDataSource(dataSource());
		sqlSessionFactory.setConfigLocation(new ClassPathResource("SqlMapConfig.xml"));
		return sqlSessionFactory;
	}

	@Bean
	DataSourceTransactionManager transactionManager() {
		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource());
		return transactionManager;
	}
}

这里的主要是数据库相关的配置,还有视图的配置,是可以分为2个文件的。如果不考虑定时器,基本也就这样了,然后就是把WebAppInitializer再改造一下,去掉配置文件的部分。

public class WebAppInitializer implements WebApplicationInitializer {

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
		ctx.register(new Class<?>[] {WebConfig.class, MvcConfig.class});
		servletContext.addListener(new ContextLoaderListener(ctx));
		ctx.setServletContext(servletContext);

		ServletRegistration.Dynamic servlet = servletContext.addServlet("spring", new DispatcherServlet(ctx));
		servlet.addMapping("*.do");
		servlet.setLoadOnStartup(1);

		FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("Set Character Encoding", new CharacterEncodingFilter("UTF-8"));
		encodingFilter.addMappingForUrlPatterns(null, true, "/");
	}
}

这样2个xml配置文件就可以去掉了。但在实际改造过程中,考虑到自己现在的项目,是有定时器模块的,同一套代码分几个服务器部署,现在的做法,就是不需要定时器的就把<import resource="classpath:app-scheduling.xml"/>注释,需要就开,而定时器开的模块也不同,这就需要能根据不同的环境配置定时器配置,怎么做才能满足这个需求?

@Configuration
public class QuartzConfig {

	@Value("${task.type}")
	private Integer type;

	@Value("${task.cron}")
	private String cron;

	@Bean
	public SchedulerFactoryBean scheduler() {
		SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
		Trigger[] triggers;
		if (type == 3) {
			triggers = new Trigger[2];
			triggers[1] = identifyTrigger2().getObject();
		} else if (type == 2) {
			triggers = new Trigger[2];
			triggers[1] = identifyTrigger1().getObject();
		} else {
			triggers = new Trigger[1];
		}
		triggers[0] = identifyTrigger().getObject();
		scheduler.setTriggers(triggers);
		return scheduler;
	}

	@Bean
	public CronTriggerFactoryBean identifyTrigger() {
		CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
		trigger.setCronExpression(cron);
		trigger.setJobDetail(identifyDetial().getObject());
		return trigger;
	}

	@Bean
	public MethodInvokingJobDetailFactoryBean identifyDetial() {
		MethodInvokingJobDetailFactoryBean detail = new MethodInvokingJobDetailFactoryBean();
		detail.setTargetBeanName("testTask");
		detail.setTargetMethod("testTask");
		detail.setConcurrent(false);
		return detail;
	}

	@Bean
	public CronTriggerFactoryBean identifyTrigger1() {
		CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
		trigger.setCronExpression(cron);
		trigger.setJobDetail(identifyDetial1().getObject());
		return trigger;
	}

	@Bean
	public MethodInvokingJobDetailFactoryBean identifyDetial1() {
		MethodInvokingJobDetailFactoryBean detail = new MethodInvokingJobDetailFactoryBean();
		detail.setTargetBeanName("testTask1");
		detail.setTargetMethod("testTask");
		detail.setConcurrent(false);
		return detail;
	}

	@Bean
	public CronTriggerFactoryBean identifyTrigger2() {
		CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
		trigger.setCronExpression(cron);
		trigger.setJobDetail(identifyDetial2().getObject());
		return trigger;
	}

	@Bean
	public MethodInvokingJobDetailFactoryBean identifyDetial2() {
		MethodInvokingJobDetailFactoryBean detail = new MethodInvokingJobDetailFactoryBean();
		detail.setTargetBeanName("testTask2");
		detail.setTargetMethod("testTask");
		detail.setConcurrent(false);
		return detail;
	}
}

这里考虑的就是这种思路,根据config.properties中配置的类型来决定,Trigger要加载哪些bean,执行什么定时任务,定时器的周期根据时间再配置在配置文件,后续就是把这个type改为Constans.curEnv,由这个常量决定装载哪些定时任务。

//加载什么配置
ctx.register(new Class<?>[] {WebConfig.class, MvcConfig.class, QuartzConfig.class});

// 修改版
if (Constants.curEnv.contains("test")) {
    ctx.register(new Class<?>[] {WebConfig.class, MvcConfig.class});
} else if (Constants.curEnv.contains("task")) {// 定时器服务
    ctx.register(new Class<?>[] {WebConfig.class, MvcConfig.class, QuartzConfig.class});
}

基本靠这个常量类的环境变量上这样就解决哪种环境配置哪个定时器的需要。基本思路就是把需要动态配置的分开,不加载对应的类就可以了。

目前暂时没找到logback的配置文件怎么动态加载,在web.xml设置了

<context-param>
	<param-name>spring.liveBeansView.mbeanDomain</param-name>
	<param-value>test</param-value>
</context-param>

logback的配置文件读取不到这个变量,打断点可以发现,logback的初始,在Spring容器的初始化之前,所以读取不到这个变量,后续再找办法,目前只能是读取固定的config.properties。

完。

04-05 07:13