之前项目里用spring mvc都没有系统记录过,这次总结在一起以便日后查看。本文使用的springmvc版本为3.2.16,mybatis版本为3.3.1。官方文档 | 打印版 | 示例工程
1、SpringMVC基本配置
在j2ee项目里使用maven添加springmvc依赖,pom.xml里添加如下内容:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>3.2.16.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>3.2.16.RELEASE</version> </dependency> </dependencies>
在web.xml里添加DispatcherServlet:
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>myapp</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
在WEB-INF下添加applicationContext.xml文件:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> </beans>
在WEB-INF下添加myapp-servlet.xml文件("myapp"这个名字要与web.xml里定义的servlet名称匹配):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" 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-3.2.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-3.2.xsd"> <!-- 启动注解驱动的Spring MVC功能,注册请求url和注解POJO类方法的映射 --> <mvc:annotation-driven /> <!-- 启动包扫描功能,以便注册带有@Controller、@Service、@repository、@Component等注解的类成为spring的bean --> <context:component-scan base-package="cn.myapp.*" /> <!-- 静态资源文件,避免被DispatcherServlet拦截 --> <mvc:resources location="/css/" mapping="/css/**"/> <mvc:resources location="/scripts/" mapping="/scripts/**"/> <mvc:resources location="/images/" mapping="/images/**"/> <!-- 对模型视图名称的解析,在请求时模型视图名称添加前后缀 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/" p:suffix=".jsp" /> </beans>
典型的SpringMVC工程包括Repository、Service、Controller和View等多层结构,其中Repository层负责封装数据库操作,Service层负责业务逻辑,Controller层负责页面跳转输出,View层是前端展现。
写一个Controller验证一下,例如:
@Controller public class TestController { /** * 简单url * @return */ @RequestMapping(value = "/welcome", method = RequestMethod.GET) public String myMethod1() { return "/login";//forward to /login.jsp } }
在浏览器里访问:http://localhost:8080/myapp/welcome,看是否转向到/login.jsp(如果还没有这个文件会报404错误)
用@RequestParam注解可以获取到request里的parameter值,用ModelAndView对象可以将数据传递给页面(相当于request.setAttribute()),例如可以在Controller里这样写:
@RequestMapping(value = "/web/users") public ModelAndView listUsers(@RequestParam(value = "p", required = false, defaultValue = "0") Integer page) { Map<String, Object> model = new HashMap<String, Object>(); model.put("discounts", userService.listUsers(page)); return new ModelAndView("users", model); }
技巧1:可以使用Spring提供的ModelMap对象简化上面的代码(参考What's the difference between ModelAndView and ModelMap?):
@RequestMapping(value = "/web/users") public String listUsers(@RequestParam(value = "p", required = false, defaultValue = "0") Integer page, ModelMap model) { model.put("discounts", userService.listUsers(page));//将数据添加到传入的model参数 return "users";//只需直接返回view的名字 }
技巧2:有些页面不需要经过Controller(例如register界面经常是这种情况),但又希望用户不要直接在浏览器地址栏里看到.jsp页面,可以直接在myapp-servlet.xml里使用下面的方式指定自动跳转,以免自己定义很多空的Controller:
<!-- 访问/register时直接跳转到/register.jsp --> <bean name="/register" class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
技巧3:要让Controller返回二进制流,可直接使用response对象。具体可参考Downloading a file from spring controllers
技巧4:如果提交表单后发现中文乱码,请在web.xml里配置Spring提供的CharacterEncodingFilter,设置为utf-8。具体介绍文章网上很多,例如这篇。
2、使用JNDI数据源连接MySQL
参考Spring DataSource JNDI with Tomcat Example。首先在pom.xml里添加dependencies:
<!-- MySQL Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency>
在myapp-servlet.xml里添加数据源bean,这个数据源(java:comp/env/jdbc/mydb)的具体属性如url、user、password是事先在容器(如tomcat的<Context>)里定义好的:
<!-- DataSource bean--> <bean id="dbDataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName" value="java:comp/env/jdbc/mydb"/> </bean>
在需要使用此数据源的Controller类里(注意:@Controller本身也是@Bean;如果容器里只定义了一个DataSource,则@Qualifier注解可以省略):
@Autowired @Qualifier("dbDataSource") private DataSource dataSource;
使用JdbcTemplate简化数据库操作:
还是先在pom.xml里添加依赖项:
<!-- Spring JDBC Support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>3.2.16.RELEASE</version> </dependency>
在myapp-servlet.xml里添加jdbcTemplate bean,其dataSource属性引用已经定义过的那个dbDataSource:
<!-- JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="false" lazy-init="false" autowire="default"> <property name="dataSource"> <ref bean="dbDataSource" /> </property> </bean>
在代码里:
@Autowired @Qualifier("jdbcTemplate") private JdbcTemplate jdbcT; SqlRowSet rs = jdbcT.queryForRowSet("select id,name from mytable"); while (rs.next()) { System.out.println(rs.getInt(1) + ", " + rs.getString(2)); }
3、集成MyBatis-3
在pom.xml里添加MyBatis依赖(最新稳定版本号在这里看)和mybatis-spring组件:
<dependency> <groupid>org.mybatis</groupid> <artifactid>mybatis</artifactid> <version>3.3.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.5</version> </dependency>
在src目录下新建mybatis-config.xml文件,:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-/mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="lazyLoadingEnabled" value="false" /> </settings> <typeAliases> <typeAlias alias="user" type="cn.myapp.model.User" /> </typeAliases> </configuration>
每个mybatis项目里都需要定义一组Mapper,它们是用来包装查询语句和字段映射的核心文件。每个Mapper可以是单一xml文件、单一java接口(配合annotation),也可以是两者结合。
- 单一接口方式:
package cn.myapp.mapper; public interface UserMapper { @Select("SELECT * FROM users WHERE id = #{userId}") User getUser(@Param("userId") int userId); }
在myapp-servlet.xml里定义成bean:
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="cn.myapp.mapper.UserMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
- 单一xml文件方式(其中resultType属性值可以是类的全名,例如cn.myapp.User,或者java.util.HashMap;也可以是在mybatis-config.xml里定义过的alias名):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.myapp.mapper.UserMapper"> <select id="getUser" resultType="cn.myapp.User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>
定义为bean的方法同上。
代码里这样调用,略复杂且引号里的名称容易写错:
SqlSession session = sqlSessionFactory.openSession(); User user = session.getUser("cn.myapp.mapper.UserMapper.getUser", 101);
- xml与java接口结合的方式(xml文件内容同上),让两个文件放在同一目录下(“If the UserMapper has a corresponding MyBatis XML mapper file in the same classpath location as the mapper interface, it will be parsed automatically by the MapperFactoryBean. ” 参考)。java接口如下:
package cn.myapp.mapper; public interface UserMapper { User getUser(int id); }
定义为bean的方法同上。代码里这样调用:
@Autowired @Qualifier("userMapper") private UserMapper userMapper; { User user = userMapper.getUser(101); }
以上三种方式,个人推荐最后一种,这样只要查看.xml文件就能看到所有查询语句和映射关系(resultmap,稍后介绍),而接口只负责提供更简单的调用方式。
注意1:对String类型的参数,接口里参数定义的@Param注解不可少,否则提示There is no getter for property named 'xx' in 'class java.lang.String错误。
注意2:Mapper文件里的参数(如#{id})的使用方法可见官方文档,如果用${id}表示不要对参数做任何转换(例如加单引号)。
注意3:如果想让返回结果是map类型,可以这样定义:resultType="java.util.HashMap",但要求sql执行结果最多只能有一条数据(即selectOne);map里的entry是类似这样的:<"id", 5>,<"name","张三">,而不是<5, "张三">, <6, "李四">,后者的实现还比较复杂,需要用result handler实现。
为了使用Mapper,需要在myapp-servlet.xml里定义一个SessionFactory bean和mapper bean(dataSource的定义在文章前面已经有了):
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis-config.xml" /> <property name="dataSource" ref="dbDataSource" /> </bean> <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="cn.myapp.mapper.UserMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
在需要查询user的地方这样使用:
int userId = ...; User user = userMapper.getUser(userId);
使用Dynamic SQL支持加参数查询:
例如希望在mapper里定义一个查询参数groupId,如果传入的groupId值大于零则返回属于指定用户组的用户列表。此时应在mapper里这样定义SQL语句:
<select id="listDiscounts" resultType="cn.myapp.User"> SELECT * FROM user WHERE 1=1 <if test="groupId > 0"> and group_id = #{groupId} </if> </select>
相应的java接口(注意@Param对于Dynamic SQL的情况不能省略,否则会报错There is no getter for property named 'xxx' in 'class java.lang.Integer 参考):
package cn.myapp.mapper; public interface UserMapper { User listUsers(@Param("groupId") int groupId); }
获取自增字段值
在mapper的<insert>里使用useGeneratedKeys和keyProperty即可将自增字段的值赋值到指定属性上,例如:
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> ... </insert>
MyBatis物理分页方案
目前使用Mybatis-PageHelper这个开源项目,需要配置一个mybatis的plugin,代码里增加一行PageHelper.startPage()代码,返回的列表用PageInfo对象包装即可。例如在service类里这样使用:
PageHelper.startPage(curPage, 10); List<User> users = userDao.listUsers(); PageInfo<User> pageInfo = new PageInfo<User>(users); return pageInfo;
这个方案有一个缺点就是页码固定从1开始,而不是从0开始,应该允许开发者设置就好了。
事务
在myapp-servlet.xml里添加TransactionManager的定义,并且启用基于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" xmlns:aop="http://www.springframework.org/schema/aop" 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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- this is the service object that we want to make transactional --> <bean id="fooService" class="x.y.service.DefaultFooService"/> <!-- enable the configuration of transactional behavior based on annotations --> <tx:annotation-driven transaction-manager="txManager"/> <!-- a PlatformTransactionManager is still required --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- (this dependency is defined somewhere else) --> <property name="dataSource" ref="dataSource"/> </bean> <!-- other <bean/> definitions here --> </beans>
为需要事务支持的bean添加@Transactional注解,例如:
// the service class that we want to make transactional @Service @Transactional public class DefaultFooService implements FooService { Foo getFoo(String fooName); Foo getFoo(String fooName, String barName); void insertFoo(Foo foo); void updateFoo(Foo foo); }
@Transactional可以用来修饰接口、类、接口中的方法和类中的public方法,官方文档建议只用于类或类中的public方法。
4、实现Restful URL
Restful基本约定:
- POST:新增一个对象
- GET:获取一个对象,例如GET /user/1
- DELETE:删除一个对象
- PUT:更新一个对象
在Controller里这样定义,就可以实现 http://www.myapp.com/profile/1 这样的url风格:
/** * 带参数的url * @param request * @param response * @param user url里的参数 * @param modelMap * @return * @throws Exception */ @RequestMapping(value = "/profile/{user}", method = RequestMethod.GET) public ModelAndView myMethod2(HttpServletRequest request, HttpServletResponse response, @PathVariable("user") String user, ModelMap modelMap) throws Exception { System.out.println("User id is: " + user); ... modelMap.put("profile", ...); return new ModelAndView("/profile", modelMap); }
浏览器里<form>的method只支持GET和POST,为了能以PUT、DELETE方法提交请求,需要在jsp里用标签来写:
<form:form action="${ctx}/profile/${user.userId}" method="put"> ... </form:form>
上面的代码运行时会转换为类似下面这个样子:
<form id="user" action="/myapp/profile/2" method="post"> <input type="hidden" name="_method" value="put"/> ... </form>
所以我们还需要在myapp-servlet.xml里配置一个filter自动处理“_method”这个参数:
<filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter>
接受JSON格式的输入(需要先引入json-lib),在Controller里这样写:
import net.sf.json.JSONObject; @RequestMapping(value = "/api/search", method = RequestMethod.POST) public Object search(@RequestBody JSONObject jReq) { int page = jReq.optInt("page"); ... }
注意,请求时需要指定请求的content-type为"application/json"(jquery处理方法如下),否则会报“415 Unsupported Media Type”的HTTP错误:
/** * 防止"http 415 Unsupported Media Type" */ $.ajaxSetup({ contentType : 'application/json' });
直接输出JSON到response:
使用@ResponseBody注解Controller里的方法,即可直接输出文本,相当于response.getWriter().print(),例如:
@RequestMapping(value = "/hello", method = RequestMethod.GET) @ResponseBody public String hello() { return "{'id':1}"; }
注意:如果@ResponseBody输出有乱码,可以在@RequestMapping里添加produces="text/html;charset=utf-8"或produces="application/json;charset=utf-8"来解决。参考链接
使用jackson自动将对象转换为json:
先pom.xml里添加依赖(注意:-asl后缀表示apache license,-lgpl后缀表示LGPL license,参考):
<!-- Jackson --> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.9.11</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.11</version> </dependency>
在myapp-servlet.xml里配置jackson自动转换:
<!-- Jackson --> <bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
注意:在Spring 4.x里这个类的名字改为了org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
在Controller里直接返回对象:
@RequestMapping(value = "/hello", method = RequestMethod.GET) @ResponseBody public Object hello() { return new User(); }
5、单元测试
首先是官方文档链接。为对springmvc的项目进行单元测试,需在项目里引入junit和spring-test依赖项:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>3.2.16.RELEASE</version> <scope>test</scope> </dependency>
测试Service
下面是一个单元测试的例子:
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations = { "file:WebContent/WEB-INF/applicationContext.xml", "file:WebContent/WEB-INF/myapp-servlet.xml" }) //使用test profile @ActiveProfiles("test") //自动回滚,避免污染数据库 @Transactional @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) public class MyServiceTest { @Test public void testInsert(){ ... } }
上面的代码中,使用@ActiveProfiles是有必要的,因为在开发环境/生产环境中我们一般在web容器中通过jndi定义数据源,但测试环境里因为没有启动web容器所以这个jndi是不存在的。为此,要在myapp-servlet.xml里定义dev和test两个profile,如下所示:
<beans profile="dev"> <bean id="dbDataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName" value="java:comp/env/jdbc/mydb" /> </bean> </beans> <beans profile="test"> <bean id="dbDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" p:driverClassName="com.mysql.jdbc.Driver" p:url="jdbc:mysql://localhost:3306/mydb?characterEncoding=utf8" p:username="root" p:password="mypassword" /> </beans>
为方便起见,在web.xml里告诉Spring缺省使用dev这个profile,以免在非测试代码的每个类里都指定profile:
<context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param>
这样测试环境就可以连接到dev数据库进行测试了。
测试Controller/REST接口
使用Spring Test提供的MockMVC对象,参考官方文档之Server-Side Tests部分。以下是一个单元测试的例子:
package com.myapp; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations = { "file:WebContent/WEB-INF/applicationContext.xml", "file:WebContent/WEB-INF/myapp-servlet.xml" }) //使用test profile @ActiveProfiles("test") //自动回滚,避免污染数据库 @Transactional @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) public class UserTest { @Autowired WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void testWelcomeMybatis() { try { mockMvc.perform(get("/user/25")).andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")).andDo(print()) .andExpect(jsonPath("$.name").value("Tom")); } catch (Exception e) { e.printStackTrace(); } } @After public void teardown() { } }
需要注意几点:
1、需要添加一些静态import才能使用status().isOK()这样的fluent语法;
2、在@WebAppConfiguration注解的测试类里,可以直接@Autowired注解WebApplicationContext实例;
3、为支持jsonpath需要在pom.xml里添加下面的依赖项。当前最新版本是2.2.0,但经测试与spring-test 3.2.16不兼容(jsonpath 1.0.0也不行),compile方法提示NoSuchMethodError,需要使用0.8.1版或0.9.0版(参考链接):
<dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>0.8.1</version> <scope>test</scope> </dependency>
6、使用AOP
利用AOP可以比较方便的实现日志输出、性能监控等业务逻辑。这里以注解方式为例介绍一下如何使用。
首先在pom.xml里添加dependencies:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>3.2.16.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.11</version> </dependency>
然后写一个Aspect类,要同时用@Component和@Aspect注解,前者定义此类为一个Bean,后者定义此类为一个Aspect:
package cn.myapp; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.After; @Component @Aspect public class LoggerAspect { @Before("execution(* cn.myapp..*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("logBefore() is running!"); } @After("execution(* cn.myapp..*.*(..))") public void logAfter(JoinPoint joinPoint) { System.out.println("logAfter() is running!"); } }
在myapp-servlet.xml里添加autoproxy支持:
<aop:aspectj-autoproxy />
现在,执行任意cn.myapp包下的任意java类里的任意方法,都会在控制台里打印出logBefore和logAfter信息。
参考链接:
以上java代码里的@Before、@After被称作“Advice”,完整的语法可参考eclipse.org上的这个链接。
Spring AOP + AspectJ annotation example
7、其他
统一异常处理
在SpringMVC里使用@ControllerAdvice注解和@ExeptionHandler注解可以实现统一的异常处理。当在Controller里throw new MyException("...")后,会自动将此异常按指定方式输出,例如输出为json:
@ControllerAdvice public class MyExceptionController { @ExceptionHandler(MyException.class) @ResponseBody public Map<String, Object> handleException(MyException exception) { Map<String, Object> map = new HashMap<>(); map.put("success", false); map.put("msg", exception.getMessage()); return map; } }
输出二进制数据
以下示例代码展示了如何将用户头像图片输出到response:
@RequestMapping(value = "/user/avatar/{userId}", method = RequestMethod.GET, produces = "image/jpeg") public void avatar(@PathVariable("userId") int userId, HttpServletResponse response) { //从文件或数据库获取图片数据 byte[] byteArray = ...; response.getOutputStream().write(byteArray); }
待续
请保留原始链接:https://bjzhanghao.com/p/84