上一篇文章简单地介绍了如何从零开始构建一个Spring MVC应用,如果我们仅仅需要做一些静态页面或者数据不变化的Web应用,那么其实这样就足够了。这当然是不现实的,我们的页面中的数据需要不断地变化,不同的用户登录进来之后应用只能看到属于自己的数据,诸如此类的需求告诉我们原本的代码框架是不够的。
对于一个功能相对齐整的Web应用,除了上一篇文章中介绍的代码框架,还需要:
- 连接和操作数据库
- 配置文件
- 单元测试(虽然可以省略,但还是建议保留)
接下来的内容就介绍上面所列举的内容,以期能创建一个比较完整的Web应用框架。
使用Mybatis操作数据库
上一篇文章中已经简单地介绍过了Mybatis中的基本概念,下面直接进入实战环节。由于要依赖第三方的工具包,pom.xml文件中需要新添加依赖如下:
<!-- database -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path-assert</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- test end -->
JDBC连接池
连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。
在项目中使用连接池是非常有必要,主要有以下好处:
- 减少连接创建时间
- 简化的编程模式
- 受控的资源使用
Java中有以下常用的开源连接池:
- C3P0
- DBCP
- Proxool
在本文中将使用C3P0来作为数据库连接池组件,并且用Spring来管理C3P0。为了使用Spring来管理应用中的所有组件,我们先在Web.xml文件中配置下面的代码,使得Spring的上下文(context)文件能够随着应用一块启动。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
插个题外话,注意Spring的上下文配置的路径为:classpath:applicationContext.xml
。在笔者刚开始学习Java的很长一段时间里,都不太明白什么是classpath,以致于常常取不到资源文件。
classpath表示一个绝对路径,但不是一个固定值。它是编译完毕后存放xxx.class文件的最顶层目录所对应的路径。譬如使用Maven编译之后通常(如果没有手动修改Maven配置)会将编译后的class文件放置在target\classes目录下,于是classpath
表示的绝对路径为:工程根路径\target\classes\
。所以,本文中的Spring的上下文配置文件的绝对路径为:工程根路径\target\classes\applicationContext.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:tx="http://www.springframework.org/schema/tx"
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-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- 配置数据连接池(C3P0) -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf8" />
<property name="user" value="test" /> <!-- 数据库用户名 -->
<property name="password" value="test" /> <!-- 数据库密码 -->
<property name="maxPoolSize" value="10" />
<property name="minPoolSize" value="1" />
<property name="initialPoolSize" value="2" />
<property name="maxIdleTime" value="20" />
<property name="acquireIncrement" value="3" />
<property name="idleConnectionTestPeriod" value="60" />
<property name="unreturnedConnectionTimeout" value="190" />
<property name="checkoutTimeout" value="20000" />
<property name="acquireRetryAttempts" value="30" />
</bean>
<!-- 配置数据库事务 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
数据库表
Mybatis的主要功能是将Mapper配置映射成对应的sql语句并执行。在介绍Mybatis的配置之前,先创建一个简单的数据表作为数据示例。
数据表User的结构如下:
字段 | 类型 | 索引 | 字段含义 |
---|---|---|---|
username | varchar(50) | 主键 | 用户名 |
password | varchar(50) | 密码 | |
role | int | 用户角色 |
笔者使用的是mysql数据库,sql语句如下:
CREATE DATABASE test;
USE test;
CREATE TABLE `test`.`User`(
`username` VARCHAR(50) COMMENT '用户名',
`password` VARCHAR(50) COMMENT '密码',
`role` INT COMMENT '角色'
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO User VALUES("test", "test", 1); # 插入一条记录
Mybatis配置
在src/main/resources
资源文件夹中创建一个spring-mybatis.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-3.0.xsd">
<!-- mybatis SqlSessionFactory配置 -->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mapper/*.xml" />
</bean>
<!-- Mapper 扫描配置 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sessionFactory" />
<property name="basePackage" value="demo.dao" />
</bean>
</beans>
在【mybatis SqlSessionFactory配置】配置项中,指定了mybatis的Mapper文件所在的位置(classpath:/mapper/*.xml
)。于是,在src/main/resources
资源文件夹创建一个mapper子目录,将所有的Mapper文件放置在这个文件夹下。
在applicationContext.xml
文件中引入spring-mybatis.xml
,使其加入spring的管理容器中。
<import resource="classpath:spring-mybatis.xml"/>
Dao接口和Mapper配置
首先创建一个demo.model
包,在这个包下创建一个名为User的类。这个类的字段与上面设计的数据表里面的字段是一一对应的。
package demo.model;
/**
* 数据模型,与数据库表里面的字段对应。
* @author xialei
*
*/
public class User {
private String username;
private String password;
private Integer role;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getRole() {
return role;
}
public void setRole(Integer role) {
this.role = role;
}
}
接着创建一个demo.dao
包,在这个包下创建一个名为UserMapper的类。这个类的作用是定义一系列数据操作方法。
package demo.dao;
import org.apache.ibatis.annotations.Param;
import demo.model.User;
public interface UserMapper {
/**
* 访问数据库,检查用户名和密码输入是否正确。
* @param username
* @param password
* @return
*/
public User check(@Param("username") String username, @Param("password") String password);
}
最后创建UserMapper所对应的Mapper文件(UserMapper.xml),mybatis会根据这个文件生成这个接口的一个实现类。
<?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" >
<!-- demo.dao.UserMapper与UserMapper定义路径一致 -->
<mapper namespace="demo.dao.UserMapper" >
<!-- demo.model.User与User的定义路径一致,下面是Java对象与数据库字段的映射关系。 -->
<resultMap id="BaseResultMap" type="demo.model.User" >
<id column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="role" property="role" jdbcType="TINYINT" />
</resultMap>
<sql id="Base_Column_List" >
username, password, role
</sql>
<!-- id必须与UserMapper中定义的方法名一致。 -->
<select id="check" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user
where username = #{username,jdbcType=VARCHAR}
and password = #{password,jdbcType=VARCHAR}
</select>
</mapper>
示例
为了测试整个框架是否已经打好,我们写一个测试页面来测试一下,就用最常用的登录功能来验证。
首先,创建一个User控制器(UserController.java)。
package demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import demo.dao.UserMapper;
import demo.model.User;
@Controller
public class UserController {
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/dologin")
public ModelAndView doLogin(String username, String password) {
User user = this.userMapper.check(username, password);
if (user == null) {
return new ModelAndView("error");
} else {
ModelAndView modelAndView = new ModelAndView("success");
modelAndView.addObject("username", username);
return modelAndView;
}
}
}
然后,创建对应的jsp页面来显示结果。
登录页面:login.jsp
,文件路径为:src/main/webapp/WEB-INF/jsp/login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录</title>
</head>
<body>
<form action="${pageContext.servletContext.contextPath}/dologin" method="post">
用户名:<input name="username" type="text" >
密码:<input name="password" type="password">
<button type="submit">登录</button>
</form>
</body>
</html>
登录成功页面:success.jsp
,文件路径为:src/main/webapp/WEB-INF/jsp/success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录成功</title>
</head>
<body>
登录成功!您的用户名为:<%=request.getParameter("username")%>
</body>
</html>
登录失败页面:error.jsp
,文件路径为:src/main/webapp/WEB-INF/jsp/error.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录失败</title>
</head>
<body>
登录失败,用户名或密码错误。
</body>
</html>
使用Jetty启动应用,在浏览器中输入http://localhost:8080/login
进入登录界面。如果输入正确的用户名和密码(test/test),则跳入success.jsp对应的页面,输入其他信息,则跳入error.jsp对应的页面。
使用配置文件
通常系统中存在常量(数据库连接URL、用户名、密码等),这些常量如果直接嵌入到Java代码中,则一旦代码编译完毕后就无法更改,如需更改则只能在源代码中改完后重新编译。当然,如果像上面一样,写在诸如applicationContext.xml
的xml文件中也是可以的。但是如果这样的文件很多,找起来就会比较困难。
比较推荐的方法是将这些常量集中写到一个常量配置文件中。Java中默认的配置文件后缀是.properties
,下面就将常量移入配置文件中。
首先,在applicationContext.xml
中加入下面的代码,表示用配置文件中的值来替换掉applicationContext.xml
中的变量引用表达式。
<context:property-placeholder location="classpath:config.properties"/>
在src/main/resources
文件夹中创建config.properties
配置文件,加入如下代码:
# ------------------ Data source ----------------
dataSource.pool.driverClass=com.mysql.jdbc.Driver
dataSource.pool.jdbcUrl=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
dataSource.pool.user=test
dataSource.pool.password=test
dataSource.pool.maxPoolSize=10
dataSource.pool.minPoolSize=1
dataSource.pool.initialPoolSize=2
dataSource.pool.maxIdleTime=20
dataSource.pool.acquireIncrement=3
dataSource.pool.idleConnectionTestPeriod=60
dataSource.pool.unreturnedConnectionTimeout=190
dataSource.pool.checkoutTimeout=20000
dataSource.pool.acquireRetryAttempts=30
同时,修改applicationContext.xml
文件代码,使用EL表达式来引用这些配置项的值。
<!-- 配置数据连接池(C3P0) -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${dataSource.pool.driverClass}" />
<property name="jdbcUrl" value="${dataSource.pool.jdbcUrl}" />
<property name="user" value="${dataSource.pool.user}" />
<property name="password" value="${dataSource.pool.password}" />
<property name="maxPoolSize" value="${dataSource.pool.maxPoolSize}" />
<property name="minPoolSize" value="${dataSource.pool.minPoolSize}" />
<property name="initialPoolSize" value="${dataSource.pool.initialPoolSize}" />
<property name="maxIdleTime" value="${dataSource.pool.maxIdleTime}" />
<property name="acquireIncrement" value="${dataSource.pool.acquireIncrement}" />
<property name="idleConnectionTestPeriod" value="${dataSource.pool.idleConnectionTestPeriod}" />
<property name="unreturnedConnectionTimeout" value="${dataSource.pool.unreturnedConnectionTimeout}" />
<property name="checkoutTimeout" value="${dataSource.pool.checkoutTimeout}" />
<property name="acquireRetryAttempts" value="${dataSource.pool.acquireRetryAttempts}" />
</bean>
这样在spring容器启动时,会用配置文件中的值替换掉对应的表达式。
单元测试
在Java体系中,JUnit无疑是最强大的单元测试工具,这里就不介绍JUnit的使用了。Spring管理的应用中,依赖关系比较复杂,而且有时会有隐蔽性,单纯的JUnit恐怕很难进行测试。下文介绍专门用于Spring应用单元测试的spring-test组件,以及用于Web应用测试的mock组件的简单使用。
简单的测试
Maven中,通常将所有的测试用例写在src/test/java
源文件夹下。下面的代码测试UserMapper这个类,这仅仅是一个样例,介绍如何使用spring-test进行测试而已,测试用例并具有可参考性。
package demo.test.dao;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import demo.dao.UserMapper;
import demo.model.User;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testDoLoginWrong() {
User user = this.userMapper.check("eroor", "error");
Assert.assertNull(user);
}
@Test
public void testDoLoginRight() {
User user = this.userMapper.check("test", "test");
Assert.assertNotNull(user);
}
}
web测试
进行web测试时,需要模拟HTTP请求。使用spring-test无需将Web应用部署到服务器就可以进行测试,非常方便实用。下面是一个示例,更多的关于spring-test的知识可以看这篇博文。
package demo.test.controller;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
*
* @author xialei
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration("src/main/webapp")
@ContextConfiguration(locations = {"classpath:applicationContext.xml", "file:src/main/webapp/WEB-INF/dispatch-servlet.xml"})
@EnableWebMvc
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
protected MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void testDoLogin() throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/dologin").param("username", "test").param("password", "test"))
.andDo(MockMvcResultHandlers.print()).andReturn();
String viewName = mvcResult.getModelAndView().getViewName();
Assert.assertEquals("success", viewName);
String value = (String)mvcResult.getModelAndView().getModelMap().get("username");
Assert.assertEquals("test", value);
}
@Test
public void testDoLoginIllegalInput() throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/dologin").param("username", "error").param("password", "error"))
.andDo(MockMvcResultHandlers.print()).andReturn();
String viewName = mvcResult.getModelAndView().getViewName();
Assert.assertEquals("error", viewName);
}
}
总结
关于使用SpringMVC+Mybatis来创建一个Web应用系列已经写完了。文章从无到有地创建了一个Web应用,知识点讲得不太细,旨在梳理创建整个应用过程中涉及到的概念、组件。阅读本系列文章可以了解到创建一个基于SpringMVC+Mybatis的Web应用是如何一步一步地搭建起来的,虽然示例比较简单,基本停留在Demo级别,但还是比较完整的。在今后创建类似Web应用时,可以直接拿来做代码骨架。
使用springmvc+mybatis创建Web应用(一)—— 相关概念,工具,搭建Web应用
Github地址:https://github.com/xialei199023/springmvc-mybati-webapp
本文由xialei原创,转载请说明出处http://hinylover.space/2016/04/11/springmvc-mybatis-createproject-demo-2/。