最近看了《Spring实战》这本书,书中提到Spring4.0以后推荐使用javaconfig配置方式,而公司的一个项目使用的是传统的XML配置方式,刚好可以拿来练练手。虽然现在Spring Boot越来越流行,极大的简化了配置过程,使大家不再是面向配置编程[笑哭脸],但还是想研究一下纯javaconfig方式配置,以了解点更深层的东西,现在通过一个demo项目演示如何零配置web.xml实现SSM框架。
目录
3. 添加过滤器Filter、监听器Listener和其他Servlet
1. 配置方式对比图
该Demo项目我是使用maven构建的,Spring版本使用的是4.3.9,MySQL版本为8.0,开发工具使用的是IntelliJ IDEA,如何在idea中搭建maven+SpringMVC项目,网上教程很多,这里就不深入讨论了,下面的文章可以参考一下
上面给出了这两种配置方式的对应文件图,先让大家有个整体的印象,后面我会逐一进行讲解,每一部分我都会贴出XML配置方式和Java配置方式的源码对比,以便大家更好的理解,那现在我们就开始吧^_^。
2. Spring MVC应用上下文配置
DispatcherServlet是Spring MVC的核心,在这里请求会第一次接触到框架,它来负责将请求路由到其他组件之中。
按照传统的方式,像DispatcherServlet这样的Servlet,以及根应用上下文会配置在web.xml文件中。但是,借助于Servlet 3规范,这种方式已经不是唯一方案了,可以通过扩展AbstractAnnotationConfigDispatcherServletInitializer类来实现。
XML配置方式(/src/main/webapp/WEB-INF/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">
<display-name>springmvcdemo</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!--设置根上下文配置文件位置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--注册ContextLoaderListener-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--注册DispatcherServlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--设置DispatcherServlet上下文配置文件位置-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!--将DispatcherServlet映射到“/”-->
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Java配置方式(/config/WebAppInitializer.java)
package com.mfun.javaconfigdemo.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指定根应用上下文配置类
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {RootConfig.class};
}
/**
* 指定DispatcherServlet配置类
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] {WebConfig.class};
}
/**
* 配置DispatcherServlet映射
*/
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
我们扩展AbstractAnnotationConfigDispatcherServletInitializer类,并复写了它的3个方法,该类在部署到Servlet 3.0容器中时,容器会自动发现它,并用来配置Servlet上下文。
- getRootConfigClasses() 方法返回带有@Configuration注解的类,将会用来配置ContextLoaderListener创建的根应用上下文中的bean,通常是驱动应用后端的中间层和数据层组件,此处使用的RootConfig配置类后面会讲到。该方法对应于web.xml中的contextConfigLocation和ContextLoaderListener处配置。
- getServletConfigClasses() 方法返回带有@Configuration注解的类,将会用来定义DispatcherServlet应用上下文中的bean,通常是Web组件的bean,此处使用的WebConfig配置类后面会讲到。该方法对应于web.xml中DispatcherServlet处配置。
- getServletMappings() 方法将一个或多个路径映射到DispatcherServlet上。该方法对应于web.xml中servlet-mapping处配置。
重要提示:此处的java配置方式仅适用于支持Servlet3.0以上版本的Web容器,如Tomcat 7 或更高版本,如果你不能满足该条件,那么别无选择,只能使用web.xml了。
另外,你还需要在maven配置文件pom.xml中添加SpringMVC依赖包:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
3. 添加过滤器Filter、监听器Listener和其他Servlet
如果你想通过Java配置方式注册过滤器filter、监听器Listener和其他Servlet的话,怎么办呢?别着急往下看:
XML配置方式(/src/main/webapp/WEB-INF/web.xml)
<!-- 字符集过滤器 -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--支持Restful请求-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Java配置方式(/config/BaseInitializer.java)
package com.mfun.javaconfigdemo.config;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
public class BaseInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 字符集过滤器
FilterRegistration.Dynamic encodingFilter =
servletContext.addFilter("charEncodingFilter", CharacterEncodingFilter.class);
encodingFilter.setInitParameter("encoding", "UTF-8");
encodingFilter.setInitParameter("forceEncoding", "true");
// 添加Filter映射路径
encodingFilter.addMappingForUrlPatterns(null, false, "/*");
// 支持Restful请求
FilterRegistration.Dynamic hiddenHttpMethodFilter =
servletContext.addFilter("hiddenHttpMethodFilter", HiddenHttpMethodFilter.class);
hiddenHttpMethodFilter.addMappingForUrlPatterns(null, false, "/*");
}
}
基于Java的初始化器允许我们定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需要创建一个新的初始化器就可以了,最简单的方式就是实现Spring的WebApplicationInitializer接口。其实查看AbstractAnnotationConfigDispatcherServletInitializer类的源码会发现它也实现了该接口,所以能够被Web容器发现。
你可以复写onStartup(),在其中添加Filter、Listener或其他Servlet,如上所示,我们添加了CharacterEncodingFilter和HiddenHttpMethodFilter,也可以添加其他Servlet、Listener:
@Override
public void onStartup(ServletContext servletContext) {
// 添加自定义Servlet
ServletRegistration.Dynamic myServlet =
servletContext.addServlet("myServlet", MyServlet.class);
myServlet.addMapping("/custom/**");
// 添加自定义监听器
servletContext.addListener(MyListener.class);
}
4. DispatcherServlet配置类
DispatcherServlet配置类(WebConfig.java)主要用于扫描注册Web组件bean和其他相关配置,以启用SpringMVC,Web组件bean主要为@Controller或@RestController注解的类。
XML配置方式(/src/main/resources/dispatcher-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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--启用SpringMVC相关注解支持-->
<mvc:annotation-driven />
<context:annotation-config />
<!--使静态资源的请求直接由Web应用服务器默认的Servlet处理-->
<mvc:default-servlet-handler />
<!--仅扫描@Controller注解的类-->
<context:component-scan
base-package="com.mfun.springmvcdemo.controller"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".html"></property>
</bean>
</beans>
Java配置方式(/config/WebConfig.java)
package com.mfun.javaconfigdemo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
// 仅扫描controller注解的类,不使用默认的过滤规则
@ComponentScan(basePackages = {"com.mfun.javaconfigdemo.controller"},
useDefaultFilters = false,
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)
})
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* 配置视图解析器
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".html");
return resolver;
}
/**
* 使静态资源的请求直接使用默认servlet处理
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
我们通过扩展WebMvcConfigurerAdapter类,并添加@EnableWebMvc注解及复写相关方法,即可配置好Spring WebMvc。
- @ComponentScan 设置为仅扫描com.mfun.javaconfigdemo.controller包下的@Controller注解的类,必须设置useDefaultFilters为false,来禁用默认过滤规则,因为默认过滤规则会扫描所有@Component注解的类,而不仅仅是includeFilters指定的。
- viewResolver() 方法配置了一个视图解析器。
- configureDefaultServletHandling() 复写该方法,通过DefaultServletHandlerConfigurer的enable()方法,使DispatcherServlet将静态资源的请求(如图片URL)自动转发到Web容器中的默认Servlet上,如果不这样设置,可能会由于DispatcherServlet中没有对应的映射方法而造成请求失败。
配置拦截器
通过复写该类的addInterceptors方法,可以添加拦截器,如登录拦截器,用于验证用户身份。
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**");
}
5. 根应用上下文配置类
根应用上下文配置类(RootConfig.java)主要用于扫描注册除Web组件之外的其他bean,以及应用相关配置,该组件通常是驱动应用后端的中间层和数据层组件,如@Service、@Repository等注解的类。
XML配置方式(/src/main/resources/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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 组件扫描 -->
<context:component-scan base-package="com.mfun.springmvcdemo">
<!-- 制定扫包规则 ,不扫描@Controller注解的JAVA类 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 引入属性文件 -->
<bean id="propertyConfigurer"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:application.properties</value>
<value>classpath:application-${spring.profiles.active:dev}.properties</value>
<value>classpath:log4j.properties</value>
</list>
</property>
</bean>
</beans>
Java配置方式(/config/RootConfig.java)
package com.mfun.javaconfigdemo.config;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@PropertySource({
"classpath:application.properties",
"classpath:application-${spring.profiles.active:dev}.properties",
"classpath:log4j.properties",
})
@ComponentScan(basePackages = {"com.mfun.javaconfigdemo"},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {EnableWebMvc.class, Controller.class})
})
public class RootConfig {
/**
* 加载属性文件,同时使@Value注解可用,解析属性占位符
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
- @ComponentScan 设置为仅扫描com.mfun.javaconfigdemo包下@Component、@Repository、@Service等注解的类,不扫描@Controller和@EnableWebMvc注解的类(已在WebConfig.java中配置扫描)。
- PropertySourcesPlaceholderConfigurer 用来加载系统属性、系统环境变量和指定的属性文件中的属性,同时使@Value注解可用,解析属性占位符。
- @PropertySource 指定需要加载的属性文件,其中 ${spring.profiles.active:dev} 用来根据部署环境来加载不同的属性文件,具体用法可参考 https://blog.csdn.net/swordsnapliu/article/details/78540902。
6. MyBatis数据库配置
Spring对常用持久化框架都提供了支持,如Hibernate、MyBatis、JDO等,并提供了一些附加服务。由于MyBatis相比Hibernate更加实用和灵活,变得越来越主流,下面我们看下如何配置它吧。
XML配置方式(/src/main/resources/jdbcConfig.xml)
该配置方式需要在/src/main/resources/applicationContext.xml文件里添加
<import resource="classpath:jdbcConfig.xml" />
来加载JDBC配置文件
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置数据源连接池,此处使用c3p0 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 基础配置 -->
<property name="jdbcUrl" value="${jdbc.url:}"></property>
<property name="driverClass" value="${jdbc.driver:}"></property>
<property name="user" value="${jdbc.username:}"></property>
<property name="password" value="${jdbc.password:}"></property>
<!-- 关键配置 -->
<property name="checkoutTimeout" value="${c3p0.checkoutTimeout:0}" />
<!--初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3-->
<property name="initialPoolSize" value="${c3p0.pool.initSize:3}" />
<!--连接池中保留的最小连接数-->
<property name="minPoolSize" value="${c3p0.pool.minSize:3}" />
<!--连接池中保留的最大连接数-->
<property name="maxPoolSize" value="${c3p0.pool.maxSize:15}" />
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数-->
<property name="acquireIncrement" value="${c3p0.acquireIncrement:3}" />
<!--控制数据源内加载的PreparedStatements数量。如果maxStatements-->
<!--与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0-->
<property name="maxStatements" value="${c3p0.maxStatements:0}" />
<!--定义了连接池内单个连接所拥有的最大缓存statements数。Default: 0-->
<property name="maxStatementsPerConnection" value="${c3p0.maxStatementsPerConnect:0}" />
<!--最大空闲时间, 空闲时间内未使用则连接被丢弃。若为0则永不丢弃,单位:秒 -->
<property name="maxIdleTime" value="${c3p0.maxIdleTime:0}" />
<!--检查连接池中所有空闲连接的间隔时间,单位为秒-->
<property name="idleConnectionTestPeriod" value="${c3p0.idleConnectionTestPeriod:0}" />
<!--如果设为 true 那么在归还连接时将校验连接的有效性。 Default: false-->
<!--异步操作,应用端不需要等待测试结果,但会增加一定的数据库调用开销-->
<property name="testConnectionOnCheckin" value="${c3p0.testConnectionOnCheckin:false}" />
</bean>
<!-- 配置Mybatis工厂 -->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 自动扫描Dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mfun.springmvcdemo.dao"></property>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--启用事务注解支持-->
<tx:annotation-driven />
</beans>
Java配置方式(/config/JdbcConfig.java)
package com.mfun.javaconfigdemo.config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
@Configuration
@EnableTransactionManagement // 启用事务注解
@MapperScan(basePackages={"com.mfun.javaconfigdemo.dao"}, // 自动扫描Mapper
sqlSessionFactoryRef="sqlSessionFactoryBean")
public class JdbcConfig {
@Value("${jdbc.driver:}")
private String driver;
@Value("${jdbc.url:}")
private String dbUrl;
@Value("${jdbc.username:}")
private String username;
@Value("${jdbc.password:}")
private String password;
@Value("${c3p0.checkoutTimeout:0}")
private int checkoutTimeout;
@Value("${c3p0.pool.initSize:3}")
private int poolInitSize;
@Value("${c3p0.pool.minSize:3}")
private int poolMinSize;
@Value("${c3p0.pool.maxSize:15}")
private int poolMaxSize;
@Value("${c3p0.acquireIncrement:3}")
private int acquireIncrement;
@Value("${c3p0.maxStatements:0}")
private int maxStatements;
@Value("${c3p0.maxStatementsPerConnect:0}")
private int maxStatementsPerConnect;
@Value("${c3p0.maxIdleTime:0}")
private int maxIdleTime;
@Value("${c3p0.idleConnectionTestPeriod:0}")
private int idleConnectionTestPeriod;
@Value("${c3p0.testConnectionOnCheckin:false}")
private boolean testConnectionOnCheckin;
/**
* 配置数据源连接池,此处使用c3p0
*/
@Bean
public DataSource dataSource() {
ComboPooledDataSource cpds = new ComboPooledDataSource();
try {
cpds.setJdbcUrl(dbUrl);
cpds.setDriverClass(driver);
cpds.setUser(username);
cpds.setPassword(password);
// 其他关键配置
poolConfig(cpds);
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return cpds;
}
private void poolConfig(ComboPooledDataSource cpds) {
// 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出
// SQLException,如设为0则无限期等待。单位毫秒,默认为0
cpds.setCheckoutTimeout(checkoutTimeout);
// 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3
cpds.setInitialPoolSize(poolInitSize);
// 连接池中保留的最小连接数
cpds.setMinPoolSize(poolMinSize);
// 连接池中保留的最大连接数
cpds.setMaxPoolSize(poolMaxSize);
// 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数
cpds.setAcquireIncrement(acquireIncrement);
// 控制数据源内加载的PreparedStatements数量。如果maxStatements
// 与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0
cpds.setMaxStatements(maxStatements);
// 定义了连接池内单个连接所拥有的最大缓存statements数。Default: 0
cpds.setMaxStatementsPerConnection(maxStatementsPerConnect);
// 最大空闲时间, 空闲时间内未使用则连接被丢弃。若为0则永不丢弃,单位:秒
cpds.setMaxIdleTime(maxIdleTime);
// 检查连接池中所有空闲连接的间隔时间,单位为秒
cpds.setIdleConnectionTestPeriod(idleConnectionTestPeriod);
// 如果设为 true 那么在归还连接时将校验连接的有效性。 Default: false
// 异步操作,应用端不需要等待测试结果,但会增加一定的数据库调用开销
cpds.setTestConnectionOnCheckin(testConnectionOnCheckin);
}
/**
* 配置Mybatis工厂
* @param dataSource
*/
@Bean(name = "sqlSessionFactoryBean")
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
return sessionFactoryBean;
}
/**
* 配置事务管理器
* @param dataSource
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
- dataSource() 方法注册一个数据源连接池bean,这里使用的是c3p0连接池,用它来连接MySQL数据库。
- sqlSessionFactoryBean() 方法注册一个MyBatis工厂bean,可以从它获得SqlSession,来操作数据库。这里我固定了bean ID,并在@MapperScan中使用。方法参数dataSource的值为 dataSource() 方法生成的数据源bean。
- @MapperScan 自动扫描指定包下的所有Dao接口,然后包下面的所有接口在编译之后都会生成相应的实现类。
- @EnableTransactionManagement 该注解开启声明式事务,使用事务时直接在访问数据库的Service方法上添加注解 @Transactional 便可。该注解作用对应于jdbcConfig.xml中的 <tx:annotation-driven /> 配置。
- transactionManager() 方法配置事务管理器,方法参数dataSource的值为 dataSource() 方法生成的数据源bean。
另外,你还需要在maven配置文件pom.xml中添加数据库相关依赖包,mysql依赖包版本请根据你的使用的mysql版本指定,确保兼容:
<!--数据库相关依赖包-->
<!--c3p0数据库连接池-->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version> <!--mysql8.0以上版本相应的依赖包版本-->
</dependency>
<!-- spring/mybatis依赖包包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
7. Redis缓存配置
Redis常被用于高并发场景,它是一个高性能的key-value存储系统,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。 Redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。
XML配置方式(/src/main/resources/cachingConfig.xml)
该配置方式需要在/src/main/resources/applicationContext.xml文件里添加
<import resource="classpath:cachingConfig.xml" />
来加载Redis缓存配置文件
<?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:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- Redis连接池的设置 -->
<bean id="redisPoolConfig"
class="redis.clients.jedis.JedisPoolConfig"
scope="prototype">
<!-- redis最大实例数 -->
<property name="maxTotal" value="${redis.pool.maxActive:8}" />
<!-- 连接池中最多可空闲maxIdle个连接 ,表示即使没有数据库连接
时依然可以保持maxIdle个空闲的连接,而不被清除,随时处于待命状态 -->
<property name="maxIdle" value="${redis.pool.maxIdle:8}" />
<!-- 最大等待时间:当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计数),超过时间则抛出异常 -->
<property name="maxWaitMillis" value="${redis.pool.maxWait:-1}" />
<!-- 是否在获取连接的时候检查有效性,如果检验失败,则从池中去除连接并尝试取出另一个 -->
<property name="testOnBorrow" value="${redis.pool.testOnBorrow:false}" />
</bean>
<!--Redis连接工厂,此处配置的为单机模式,-->
<!--如需要高可用,可使用哨兵模式Sentinel或集群模式Cluster-->
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg ref="redisPoolConfig" />
<property name="database" value="${redis.database:0}" />
<property name="hostName" value="${redis.hostname:}" />
<property name="port" value="${redis.port:6379}" />
<property name="timeout" value="${redis.timeout:2000}" />
</bean>
<!--Redis模板-->
<bean id="myRedisTemplate" class="com.mfun.springmvcdemo.util.MyRedisTemplate">
<constructor-arg ref="redisConnectionFactory" />
</bean>
<!--启用缓存注解支持,如@Cacheable-->
<cache:annotation-driven />
<!--Redis缓存管理器,主要用于注解式缓存,如@Cacheable,ID必须为cacheManager,否则会报错-->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg ref="myRedisTemplate" />
<property name="defaultExpiration" value="${redis.defaultExpiration:0}" />
</bean>
</beans>
Java配置方式(/config/CachingConfig.java)
package com.mfun.javaconfigdemo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
@EnableCaching // 启用缓存注解支持,如@Cacheable
public class CachingConfig {
@Value("${redis.hostname:}")
private String host;
@Value("${redis.port:6379}")
private int port;
@Value("${redis.database:0}")
private int database;
@Value("${redis.timeout:2000}")
private int timeout;
@Value("${redis.pool.maxActive:8}")
private int poolMaxActive;
@Value("${redis.pool.maxIdle:8}")
private int poolMaxIdle;
@Value("${redis.pool.maxWait:-1}")
private long poolMaxWait;
@Value("${redis.pool.testOnBorrow:false}")
private boolean poolTestOnBorrow;
@Value("${redis.defaultExpiration:0}")
private long defaultExpiration;
/**
* Redis缓存管理器,主要用于注解式缓存,如@Cacheable
*/
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
cacheManager.setDefaultExpiration(defaultExpiration);
return cacheManager;
}
/**
* Redis模板
*/
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer替换默认的序列化规则
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 调用后初始化方法,没有它将抛出异常
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* Redis连接工厂,此处配置的为单机模式,
* 如需要高可用,可使用哨兵模式Sentinel或集群模式Cluster
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(redisPoolConfig());
connectionFactory.setDatabase(database);
connectionFactory.setHostName(host);
connectionFactory.setPort(port);
connectionFactory.setTimeout(timeout);
// 调用后初始化方法,没有它将抛出异常
connectionFactory.afterPropertiesSet();
return connectionFactory;
}
/**
* Redis连接池设置
*/
private JedisPoolConfig redisPoolConfig() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// redis最大实例数
poolConfig.setMaxTotal(poolMaxActive);
// 连接池中最多可空闲maxIdle个连接,
// 表示即使没有数据库连接时依然可以保持maxIdle个空闲的连接,
// 而不被清除,随时处于待命状态。
poolConfig.setMaxIdle(poolMaxIdle);
// 最大等待时间:当没有可用连接时,
// 连接池等待连接被归还的最大时间(以毫秒计数),
// 超过时间则抛出异常
poolConfig.setMaxWaitMillis(poolMaxWait);
// 是否在获取连接的时候检查有效性,如果检验失败,则从池中去除连接并尝试取出另一个
poolConfig.setTestOnBorrow(poolTestOnBorrow);
return poolConfig;
}
}
- @EnableCaching 开启注解驱动的缓存,它会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)。根据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或从缓存中移除某个值。该注解作用对应于cachingConfig.xml中的 <cache:annotation-driven /> 配置。
@EnableCaching默认模式为AdviceMode.PROXY,,是基于Spring AOP,会造成类内部调用同类方法时,@Cacheable等缓存注解不生效,解决方法:你可以使用@EnableCaching(mode=AdviceMode.ASPECTJ),启用AdviceMode.ASPECTJ模式,同时需要在ClassPath中添加 spring-aspects.jar,注意此模式的注解只能作用于类上,接口上将不生效。
具体可看 https://www.cnblogs.com/cyhbyw/p/8615816.html
- redisConnectionFactory() 方法配置Redis连接工厂,这里通过Jedis(Redis的Java客户端)连接Redis,为了解决线程安全和资源浪费问题,同时配置了JedisPool。
- redisTemplate() 方法生成了一个Redis模板bean,通过这个bean我们可以对Redis进行操作。这里我配置的为单机模式, 如需要高可用,可使用哨兵模式Sentinel或集群模式Cluster,这里就不深入讨论了。键值的序列化方式视情况而定,我这里key按String格式序列化,value按Json格式序列化。方法参数connectionFactory的值为 redisConnectionFactory() 方法生成的连接工厂bean。
- cacheManager() 方法生成一个RedisCacheManager bean,主要用于注解式缓存,它会与一个Redis服务器协作,并通过RedisTemplate将缓存条目存储到Redis中。需要注意的是bean的ID必须为cacheManager,否则使用缓存注解时会报错。方法参数redisTemplate的值为 redisTemplate() 方法生成的Redis模板bean。
另外,你还需要在maven配置文件pom.xml中添加数据库相关依赖包,spring-data-redis依赖包版本请根据你的使用的jedis版本指定,确保兼容:
<!-- Redis缓存系统的依赖-->
<!-- Redis客户端jedis依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<!--该版本与jedis2.9.0兼容,不可升高-->
<version>1.8.1.RELEASE</version>
</dependency>
<!-- JSON依赖包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
8. 支持文件上传
要支持文件上传,我们必须配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。Spring内置了2个MultipartResolver的实现供我们选择:
- CommonsMultipartResolver: 使用Jakarta Commons FileUpload解析multipart请求;
- StandardServletMultipartResolver: 依赖于Servlet3.0对multipart请求的支持(始于Spring 3.1)。
这里我们以CommonsMultipartResolver举例说明如何配置:
XML配置方式(在 /src/main/resources/dispatcher-servlet.xml 中添加如下代码)
<!-- 支持上传文件 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver" >
<!-- 100M -->
<property name="maxUploadSize" value="104857600"></property>
<property name="defaultEncoding" value="UTF-8"></property>
</bean>
Java配置方式(在 /config/WebConfig.java 中添加如下代码)
/**
* 配置文件上传,bean的id必须为multipartResolver,否则会造成request转换错误
*/
@Bean("multipartResolver")
public MultipartResolver multipartResolver(){
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(104857600);
multipartResolver.setDefaultEncoding("UTF-8");
return multipartResolver;
}
特别注意:bean的id必须为multipartResolver,否则会造成request转换错误。
9. CORS跨域请求配置
跨域指的是JavaScript通过XMLHttpRequest请求数据时,调用JavaScript的页面所在的域和被请求页面的域不一致。对于网站来说,浏览器出于安全考虑是不允许跨域。另外,对于域相同,但端口或协议不同时,浏览器也是禁止的。
一般解决跨域请求问题常用种方法有JSONP、CORS、Nginx反向代理等多种方式,现在较流行的方式是通过CORS解决跨域问题,我们来看看如何配置:
XML配置方式(在 /src/main/resources/dispatcher-servlet.xml 中添加如下代码)
<!--CORS跨域请求支持-->
<mvc:cors>
<mvc:mapping path="/**"
allowed-headers="*"
allowed-methods="GET,POST,PUT,DELETE,OPTIONS,HEAD"
allowed-origins="${cors.allowedOrigins:}"
allow-credentials="true"
max-age="3600" />
</mvc:cors>
Java配置方式(在 /config/WebConfig.java中添加如下代码)
/**
* CORS跨域请求支持
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*") // 允许的header
.allowedMethods("OPTIONS","HEAD","GET","POST","PUT","DELETE") // 允许的请求方法
.allowedOrigins(allowedOrigins.split(",")) // 允许的请求域
.allowCredentials(true)
.maxAge(3600); // 预检测结果缓存时间
super.addCorsMappings(registry);
}
10. 定时任务配置
在项目日常开发过程中,经常需要定时任务来帮我们做一些工作,如清理日志,Spring内置了这一功能,配置也非常的简单,如下:
XML配置方式(在 /src/main/resources/applicationContext.xml 中添加如下代码)
<!--启用 @Scheduled 注解支持,实现定时器功能-->
<task:annotation-driven />
Java配置方式(在 /config/RootConfig.java中类级别添加如下注解)
@EnableScheduling
public class RootConfig {
}
配置后在需要定时执行的方法上添加@Scheduled注解即可生效,具体使用方法请自行搜索,网上教程很多。
11. 完成业务代码
我仅为了测试搭建的环境是否工作,所以这里的业务逻辑很简单,就是添加用户、修改和获取用户信息,而且没有做太多异常判断之类的工作,懒人说的就是我 [尬笑脸],具体代码如下:
Mysql数据库中创建 tb_user 表
CREATE TABLE 'tb_user' (
'id' bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
'name' varchar(30) NOT NULL DEFAULT '',
'sex' char(2) NOT NULL DEFAULT '',
'create_time' datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
'modified_time' datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT,
PRIMARY KEY ('id')
);
/model/User.java
package com.mfun.javaconfigdemo.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.sql.Timestamp;
public class User {
private Long id;
private String name;//姓名
private String sex;//性别
private Timestamp createTime;//创建时间
private Timestamp modifiedTime;//修改时间
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
public Timestamp getCreateTime() {
return createTime;
}
public void setCreateTime(Timestamp createTime) {
this.createTime = createTime;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
public Timestamp getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(Timestamp modifiedTime) {
this.modifiedTime = modifiedTime;
}
}
/dao/UserDao.java
package com.mfun.javaconfigdemo.dao;
import com.mfun.javaconfigdemo.model.User;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao {
@Results(id = "UserDao", value = {
@Result(column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "sex", property = "sex"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "modified_time", property = "modifiedTime")
})
@Select("SELECT * FROM tb_user WHERE id=#{arg0}")
User getUserById(Long id);
@Insert("INSERT INTO tb_user(name, sex) VALUES(#{name}, #{sex})")
@Options(useGeneratedKeys = true)
int add(User userInfo);
@Update("UPDATE tb_user SET name=#{arg1.name}, sex=#{arg1.sex}, modified_time=now() WHERE id=#{arg0}")
int updateById(Long id, User user);
}
/service/UserService.java
package com.mfun.javaconfigdemo.service;
import com.mfun.javaconfigdemo.dao.UserDao;
import com.mfun.javaconfigdemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 调用方法前,先检索该缓存是否存在,如果存在,直接返回缓存中的值,
// 如果不存在,调用方法后,将结果缓存起来
@Cacheable(cacheNames="userCache", key="'user_' + #p0")
public User getUserById(Long id) {
return userDao.getUserById(id);
}
// 使用事务
@Transactional(rollbackFor = Exception.class)
// 调用方法后,将结果缓存起来
@CachePut(cacheNames="userCache", key="'user_' + #p0.getId()")
public User addUser(User user) throws Exception {
int result = userDao.add(user);
if(result <= 0) {
throw new Exception("用户添加失败");
}
User newUser = getUserById(user.getId());
return newUser;
}
// 使用事务
@Transactional(rollbackFor = Exception.class)
// 更新用户信息,同时更新缓存
@CachePut(cacheNames="userCache", key="'user_' + #p0")
public User updateUser(Long id, User user) throws Exception {
int result = userDao.updateById(id, user);
if(result <= 0) {
throw new Exception("用户更新失败");
}
User newUser = getUserById(id);
return newUser;
}
}
/controller/UserController.java
package com.mfun.javaconfigdemo.controller;
import com.mfun.javaconfigdemo.model.User;
import com.mfun.javaconfigdemo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
private UserService userService;
private final static Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return user;
}
@RequestMapping(method = RequestMethod.POST)
public User addUser(@RequestBody User user) {
User newUser = null;
try {
newUser = userService.addUser(user);
} catch (Exception e) {
logger.error(e.getMessage());
}
return newUser;
}
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public User updateUser(@PathVariable Long id, @RequestBody User user) {
User userUpdated = null;
try {
userUpdated = userService.updateUser(id, user);
} catch (Exception e) {
logger.error(e.getMessage());
}
return userUpdated;
}
}
12. 测试结果
这里我使用idea自带的 Test RESTful Web Service 工具进行测试,Let's go!
- 添加用户
测试参数:
name: 张三,sex:男
Response返回数据:
数据库记录:
- 获取用户信息
测试参数:
ID为1的用户信息
Response返回数据:
- 修改用户信息
测试参数:
修改ID为1的用户信息为 name: 李红,sex: 女
Response返回数据:
数据库记录:
13. 总结
本文提到了关于Spring的两种配置方式,推荐使用类型安全并且比XML更强大的JavaConfig,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。当然对于这两种配置方式并不是互斥关系,相反,他们能够相互融合,你可以根据具体情况灵活使用,从而更有效得完成环境搭建。
源码地址:https://github.com/jming115/javaconfigdemo