小型的用户数据管理系统,实现对用户数据的CRUD,并提供Excel的下载。
学习目标:
使用技术:
Spring + SpringMVC + Mybatis
数据库:mysql
前端:Jquery EasyUI
junit
junit
test
org.springframework
spring-webmvc
org.springframework
spring-jdbc
org.springframework
spring-aspects
org.mybatis
mybatis
org.mybatis
mybatis-spring
mysql
mysql-connector-java
org.slf4j
slf4j-log4j12
com.fasterxml.jackson.core
jackson-databind
com.alibaba
druid
jstl
jstl
javax.servlet
servlet-api
provided
javax.servlet
jsp-api
provided
com.github.pagehelper
pagehelper
3.7.5
com.github.jsqlparser
jsqlparser
0.9.1
joda-time
joda-time
org.apache.poi
poi
3.10.1
com.github.abel533
mapper
2.3.4
org.apache.tomcat.maven
tomcat7-maven-plugin
8080
/
如果新创建的web工程里没有该文件,可以手动创建WEB-INF目录及web.xml;也可以通过如下方式自动创建:
web.xml的内容如下
要配置的内容有:
usermanage
contextConfigLocation
classpath:spring/applicationContext*.xml
org.springframework.web.context.ContextLoaderListener
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF8
encodingFilter
/*
usermanage
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/usermanage-servlet.xml
1
usermanage
/
index.jsp
1.applicationContext.xml文件应该放在什么位置?
参照web.xml中配置,spring容器的监听器加载的规则是:spring/applicationContext*.xml,所以文件应该放在src/main/ resources目录下的spring目录下。
2.创建spring目录并在其目录下新建applicationContext.xml文件
applicationContext.xml中内容:
classpath:jdbc.properties
同时又需要引入jdbc.properties资源文件以及log4j.properties
由于applicationContext.xml中数据源的连接信息是配置在jdbc.properties资源文件中的,所以这里需要引入该资源文件,参考mybatis工程,可直接copy过来
jdbc.properties内容如下:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis
jdbc.username=root
jdbc.password=root
3.创建spring扫描包对应的目录
如上图,参照applicationContext.xml中,注解扫描的包路径,创建在src/main/java目录下创建对应的包。如下图:
1.springmvc的配置文件应该怎么命名?放在什么位置?
如图所示,该图来自web.xml中关于springmvc的入口的设置,此处通过contextConfigLocation指定springmvc的配置路径为classpath下的spring目录,并以{servlet-name}-servlet.xml命名。
2.在spring目录下创建usermanage-servlet.xml
内容如下:
3.创建usermanage-servlet.xml配置文件中的注解扫描对应的包目录
参考综合练习课前资料:
把jsp页面导入到view目录下,把静态资源中的js导入到webapp目录下。导入后,目录结构:
springmvc和spring是同一个体系下的,在没有特殊需求下是不需要整合
WEB-INF下的资源,是不能直接访问的,必须通过内部跳转,我们需要controller
创建UserController:
@RequestMapping("user")
@Controller
public class UserController {
@RequestMapping(value="users")
public String toUsers(){
return "users";
}
}
启动tomcat之后的访问效果:
但是我们发现所有静态资源都404了,在jsp页面中引入静态资源的地方,检测路径也没有问题,那为什么报错呢?
原因:由于我们设置的DispatchServlet的映射规则是"/",所以页面中所有请求将被拦截,也包括静态资源,后端的Handler是无法处理静态资源的,所以会导致访问静态资源会404
解决方案:
Springmvc提供了一种解决方案就是在配置文件中添加设置:
在usermanage-servlet.xml中配置如下:
刷新页面后,页面正常显示,说明静态资源可以正常访问了:
在spring目录下新建applicationContext-mybatis.xml,这样不同的框架或工具跟spring的整合分开配置,方便管理
之前我们是如何获取操作数据库的sqlSession对象的?
之前构建sqlSessionFactory的方法还是比较麻烦的,当mybatis遇到spring之后,这个问题变的非常简单了。
在mybatis-spring的整合包下,存在一个sqlSessionFactoryBean,它的作用就是在spring容器中生产sqlSessionFactory的工厂Bean
sqlSessionFactoryBean源码位置:
查看sqlSessionFactoryBean源码注释:
如图所示,需要两个参数,这两个参数
结论:应该在applicationContext-mybatis.xml中配置SqlSessionFactoryBean,并且配置dataSource以及configLocation的属性
配置applicationContext-mybatis.xml:
我们之前是怎么实现动态代理的?
Mapper接口的动态代理实现,需要满足以下条件:
1.映射文件中命名空间和Mapper接口的全路径一致
2映射文件中的statementId与Mapper接口的方法名保持一致
3.映射文件中的statement的ResultType必须和mapper接口方法的返回类型一致(即使不采用动态代理,也要一致)
4.映射文件中的statement的parameterType必须和mapper接口方法的参数类型一致(不一定,该参数可省略)
那么sqlSessionFactory交给spring管理了,那么mybatis的mapper接口的动态代理实现能不能也交给spring进行管理呢?
1.参照源码注释
在mybatis-spring的整合包下,存在MapperFactoryBean这样一个工厂bean,它可以帮咱们完成:
注释中提供了使用案例:
2.参照mybatis整合spring的官方文档:
在applicationContext-mybatis.xml中,将Mapper接口交给spring管理:
创建cn.itcast.usermanage.mapper目录及UserMapper接口:
思路:
1.在UserMapper接口中定义一个方法(根据id查询用户信息)
2.在mybatis/mappers目录下新建接口对应的UserMapper.xml映射文件,并定义根据id查询用户的statement
3.创建UserMapper接口的junit test cast,在setup方法中通过spring容器初始化userMapper对象
UserMapper接口:
public interface UserMapper {
/**
* 根据id获取User信息
* @return
*/
public User queryUserById(Long id);
}
给UserMapper接口创建junit测试用例UserMapperTest,内容:
public class UserMapperTest {
public UserMapper userMapper;
@Before
public void setUp() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/applicationContext.xml",
"spring/applicationContext-mybatis.xml");
this.userMapper = context.getBean(UserMapper.class);
}
@Test
public void testQueryUserById() {
System.out.println(this.userMapper.queryUserById(1l));
}
}
需要优化的配置:
Mapper接口的配置太麻烦,每次都要去配置,并且每次都要配置多行。
在mybatis-spring的整合包中,提供了MapperScannerConfigurer接口扫描类:
类中有以下属性
需要配置basePackage以及sqlSessionFactory属性,但是setSqlSessionFactory方法已过期,推荐使用setSQLSessionFactoryBeanName
查看MapperScannerConfigurer的注释:
如果有多个sqlSessionFactory时,以逗号或者分号隔开,如果只有一个sqlSessionFactory时,可以省略sqlSessionFactoryBeanName的配置。
而我们只有一个sqlSessionFactory,所以,只需要在applicationContext-mybatis.xml中配置:
解决mybatis的resource配置方式,造成的麻烦(每次都要配置)
解决mybatis的package包扫描,造成的配置和java耦合。
可参考后一篇文章
查询用户信息,要清楚下面几个问题
Jsp页面获取数据的请求路径,参见jsp页面中的url;
而Controller方法的返回值,参考easyUI的官方案例:
Json文件的内容:
由此可见返回值是json数据,数据中包括两个字段:{total:总条数,rows:显示在表格中的具体数据}
请求参数,参考提交表单信息如下:
这就决定了Controller方法
请求路径是:http://localhost:8081/user/list
返回值:{“total”:总条数,“rows”:当前用户信息}
方法的参数:@RequestParam(“page”) Integer pageNum, @RequestParam(“rows”) Integer pageSize
为了提高代码可复用性,可以将easyUI的返回值包装成一个通用pojo对象
package cn.itcast.usermanage.pojo;
import java.util.List;
public class EasyUIResult {
private Long total;
private List> rows;
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
public List> getRows() {
return rows;
}
public void setRows(List> rows) {
this.rows = rows;
}
}
@ResponseBody
@RequestMapping("list")
public EasyUIResult queryUsersByPage(@RequestParam("page")Integer pageNum, @RequestParam("rows")Integer pageSize){
return this.userService.queryEasyUIResult(pageNum, pageSize);
}
接口:
public interface UserService {
/**
* 分页获取用户信息
* @param pageNum
* @param pageSize
* @return
*/
EasyUIResult queryEasyUIResult(Integer pageNum, Integer pageSize);
}
实现类:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public EasyUIResult queryEasyUIResult(Integer pageNum, Integer pageSize) {
// 第一个参数是从那条开始,第二个参数是查询多少条
List userList = this.userMapper.queryUsersByPage((pageNum-1)*pageSize, pageSize);
EasyUIResult easyUIResult = new EasyUIResult();
easyUIResult.setTotal(15l);
easyUIResult.setRows(userList);
return easyUIResult;
}
}
注意:service层想被注解扫描,需要在实现类上加@service注解,而非是接口上
因为在applicationContext中,将mapper接口交给spring管理了,所以此处可以使用@auotowired注解
使用动态代理后,就不需要写userMapper的实现类了
public interface UserMapper {
/**
* 分页查询用户信息
* @param start
* @param pageSize
*/
public List queryUsersByPage(@Param("start")Integer start, @Param("pageSize")Integer pageSize);
}
注意:方法中的参数,必须添加@Param注解,这样在mybatis中才能通过#{name}获取
注意返回类型分两种
我们是否可以自定义哪些属性需要被转json,哪些不用
解决方法:在实体类的对应属性上,添加@JsonIgnore注解,不同的json转换器,忽略转换的注解是不用的,我们这边是jackson-databind包下的json
我们前面的分页实现了怎么样的问题?
1.我们需要给每一个sql添加分页逻辑,还要保留不分页的sql
2.不同数据库分页方式不同
3.需要手动查询总条数信息
com.github.pagehelper
pagehelper
3.7.5
com.github.jsqlparser
jsqlparser
0.9.1
设置插件
查询所有用户信息,分页交给分页插件来完成。
UserMapper接口添加查询所有用户的方法:
/**
* 查询所有用户
* @return
*/
public List queryUserAll();
UserMapper配置添加查询所有用户的Statement:
改造UserServiceImpl,修改获取分页数据的Service方法:
@Override
public EasyUIResult queryEasyUIResult(Integer pageNum, Integer pageSize) {
// 在查询方法调用之前,调用分页插件的静态方法,中间最好不要隔任何代码
PageHelper.startPage(pageNum, pageSize);
// 第一个参数是从那条开始,第二个参数是查询多少条
// List userList = this.userMapper.queryUsersByPage((pageNum-1)*pageSize, pageSize);
List userList = this.userMapper.queryUserAll();
// 初始化pageInfo对象,所有的分页参数都可以在该对象中获取
PageInfo pageInfo = new PageInfo<>(userList);
EasyUIResult easyUIResult = new EasyUIResult();
easyUIResult.setTotal(pageInfo.getTotal());
easyUIResult.setRows(pageInfo.getList());
return easyUIResult;
}
你需要进行分页的Mybatis方法前调用PageHelper.startPage静态方法即可,紧跟在这个方法后的第一个Mybatis查询方法会被进行分页。
用PageInfo对结果进行包装,并获取分页信息,参见pageInfo的源码:
为了保证数据的一致性及原子性,Service的增删改操作应该添加事务。
接下来我们测试事务:
1、在Mapper接口中添加一个新增用户的方法
2、在Mapper映射文件中配置对应的Statement
3、在Service接口中添加一个新增用户的接口方法,该方法用来新增2个用户信息,要成功都成功,要失败都失败
4、在Service接口的实现类中实现该方法
/**
* 新增用户
* @param user
* @return
* @throws Exception
*/
public int addUser(User user) throws Exception;
INSERT INTO tb_user (
user_name,
password,
name,
age,
sex,
birthday,
created,
updated
)
VALUES
(
#{userName},
#{password},
#{name},
#{age},
#{sex},
#{birthday},
now(),
now()
);
/**
* 测试事务
* @param user1
* @param user2
*/
public void addUsers(User user1, User user2);
@Override
public void addUsers(User user1, User user2) {
this.userMapper.addUser(user1);
// 制造异常
int i=1/0;
this.userMapper.addUser(user2);
}
1.不读取applicationContext-transaction.xml文件时
执行时报异常
在数据库中新增了一条记录,没有保证crud的一致性
2.读取applicationContext-transaction.xml文件时
在数据库中就没有问题
之前进行页面跳转的时候,如果有多个页面,就需要多个方法,例如跳转到user.jsp就要写一个handler,返回值是user,跳转到user_add.jsp,又要再写一个handler,返回值是user_add,我们可以使用占位符的方式来达到页面跳转的目的
UserController中,新增通用的页面跳转方法:
@RequestMapping(value="/page/{pageName}")
public String toUserAdd(@PathVariable("pageName") String pageName){
return pageName;
}
1.解析及调整页面
users.jsp中toolbar变量的定义,找到新增按钮:
user-add.jsp中,提交事件:
触发submitForm()函数:
submitForm()函数,首先做表单验证;验证通过发送ajax请求,请求路径为:/user/save,请求参数:form表单的序列化为字符串,响应数据:{status:200}
表单验证的依据:
Form表单序列化结果测试:
2.编写UserController
如上图所示,在点击提交后,触发了ajax事件,请求/user/save,并返回一个json类型数据,这个json中只有一个属性status
/**
* 请求路径:/save
* 方法返回值:{status:200}
* 参数:User对象
*/
@RequestMapping(value="save")
@ResponseBody
public Map addUser(User user){
Map map = new HashMap<>();
try {
// 调用userService的新增方法
Boolean b = this.userService.addUser(user);
if (b) {
map.put("status", "200");
} else {
map.put("status", "500");
}
} catch (Exception e) {
map.put("status", "500");
e.printStackTrace();
}
return map;
}
3.编写UserService及其实现
@Override
public Boolean addUser(User user) throws Exception {
int count = this.userMapper.addUser(user);
if(count>0){
return true;
}
return false;
}
如果userMapper的方法不抛异常(此处我们抛异常了)
新增用户,程序写完添加用户,无法保存。控制台报异常:类型转换错误
这是因为请求参数中的日期都是string类型的
而user类是用Date(java.util.Date)类来接收,自然报这个错误
在日常工作中,经常要根据日期为条件查询数据,springmvc怎么直接接受date类型的参数呢?
删除用户时,传参是用户id数组,名为ids,所以我们在Controller中通过String[] ids来接收,返回值依旧是json类型
service层方法
statement:使用动态sql
delete from tb_user where id in
#{id}
Users.jsp页面的js代码:
注意:导出功能的js可能有浏览器兼容性问题(点击导出按钮页面没有反应)
获取分页信息的js分析:
在pom.xml中引入:
org.apache.poi
poi
3.10.1
excel也是一种视图,所以也要使用modelAndView,用model来保存数据,通过view来定位视图
springmvc提供了View接口,默认有一些抽象的实现。比如:excel的抽象实现,pdf的抽象实现
其他代码包括Controller及Service的编写,不受影响。
继承excel的抽象类,重写buildExcelDocument:
public class UserExcelView extends AbstractExcelView{
@Override
protected void buildExcelDocument(Map model,
HSSFWorkbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
//从model中获取userList
@SuppressWarnings("unchecked")
List userList = (List) model.get("userList");
//创建Excel的sheet
HSSFSheet sheet = workbook.createSheet("会员列表");
//创建标题行
HSSFRow header = sheet.createRow(0);
header.createCell(0).setCellValue("ID");
header.createCell(1).setCellValue("用户名");
header.createCell(2).setCellValue("姓名");
header.createCell(3).setCellValue("年龄");
header.createCell(4).setCellValue("性别");
header.createCell(5).setCellValue("出生日期");
header.createCell(6).setCellValue("创建时间");
header.createCell(7).setCellValue("更新时间");
String DATE = "yyyy-MM-dd";
String DATE_TIME = "yyyy-MM-dd hh:mm:ss";
//填充数据
int rowNum = 1;
for (User user : userList) {
HSSFRow row = sheet.createRow(rowNum);
row.createCell(0).setCellValue(user.getId());
row.createCell(1).setCellValue(user.getUserName());
row.createCell(2).setCellValue(user.getName());
row.createCell(3).setCellValue(user.getAge());
String sexStr;
if (user.getSex() == 1) {
sexStr = "男";
} else if (user.getSex() == 2) {
sexStr = "女";
} else {
sexStr = "未知";
}
row.createCell(4).setCellValue(sexStr);
row.createCell(5).setCellValue(new DateTime(user.getBirthday()).toString(DATE));
row.createCell(6).setCellValue(new DateTime(user.getCreated()).toString(DATE_TIME));
row.createCell(7).setCellValue(new DateTime(user.getUpdated()).toString(DATE_TIME));
rowNum++;
}
//设置相应头信息,以附件形式下载并指定文件名
response.setHeader("Content-Disposition", "attachment;filename=" + new String("会员列表.xls".getBytes(),"ISO-8859-1"));
}
}
将自定义视图注册到springmvc的容器,并指定视图名称。
Usermanage-servelet.xml配置文件:
但是仅仅这样还不够,因为此时我们的视图解析器是InternalResourceResolver,依然会被解析成jsp视图。所以我们还需要定义一个新的视图解析器
查看ViewResolver接口的所有实现,发现了熟悉的身影BeanNameViewResolver(把Bean的name作为视图解析器依据),注册该视图解析器
Usermanage-servelet.xml配置文件:
注意:
如果不配置视图解析器的顺序(order属性),运行tomcat测试,点击导出按钮,页面依然跳转向userExcel.jsp页面,由于没有该jsp页面,所以会报404
原因:
在SpringMVC中,如果定义多个视图解析器,需要指定每个视图解析的顺序(即order属性)。如果不指定顺序,DispatchServlet会按照配置的顺序调用试图解析器。而内部资源的视图解析器,不管有没有相应的视图,总有返回值;其他的视图解析器,只有视图存在的情况下,才有返回值。
所以默认顺序下,执行到内部资源视图解析器时,直接返回userExcel.jsp视图,不再去找其他试图解析器进行解析。
解决:
如上图,配置order属性,并把value的值设置为1,或者把BeanNameViewResolver的配置放在InternalResourceViewResolver上面。