概述
学习SpringMVC我们先来回顾下现在web程序是如何做的,咱们现在web程序大都基于三层架构来实现。
三层架构
-
浏览器发送一个请求给后端服务器,后端服务器现在是使用Servlet来接收请求和数据
-
如果所有的处理都交给Servlet来处理的话,所有的东西都耦合在一起,对后期的维护和扩展极为不利
-
将后端服务器Servlet拆分成三层,分别是
web
、service
和dao
- web层主要由servlet来处理,负责页面请求和数据的收集以及响应结果给前端
- service层主要负责业务逻辑的处理
- dao层主要负责数据的增删改查操作
-
servlet处理请求和数据的时候,存在的问题是一个servlet只能处理一个请求
-
针对web层进行了优化,采用了MVC设计模式,将其设计为
controller
、view
和Model
- controller负责请求和数据的接收,接收后将其转发给service进行业务处理
- service根据需要会调用dao对数据进行增删改查
- dao把数据处理完后将结果交给service,service再交给controller
- controller根据需求组装成Model和View,Model和View组合起来生成页面转发给前端浏览器
- 这样做的好处就是controller可以处理多个请求,并对请求进行分发,执行不同的业务操作。
随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不是需求,所以异步调用慢慢的走到了前台,是现在比较流行的一种处理方式。
- 因为是异步调用,所以后端不需要返回view视图,将其去除
- 前端如果通过异步调用的方式进行交互,后台就需要将返回的数据转换成json格式进行返回
- SpringMVC主要负责的就是
- controller如何接收请求和数据
- 如何将请求和数据转发给业务层
- 如何将响应数据转换成json发回到前端
介绍了这么多,对SpringMVC进行一个定义
-
SpringMVC是一种基于Java实现MVC模型的轻量级Web框架
-
优点
- 使用简单、开发便捷(相比于Servlet)
- 灵活性强
小总结
SpringMVC属于Spring,是Spring的一部分
SpringMVC是用来和Servlet技术功能等同,均属于web层或者表现层开发技术
SpringMVC与Servlet相比,开发起来更简单快捷,能用更少的代码完成表现层代码的开发
入门
pom文件
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
servlet的坐标为什么需要添加provided ?
- scope是jar包依赖作用范围的描述,
- 如果不设置默认是compile,那么在编译,运行,测试时均有效
- 如果运行有效的话就会和我们自己增加的tomcat7插件中的servlet冲突,导致启动出错
- provided代表的是该包只在编译和测试的时候用,运行的时候无效直接使用tomcat中的
创建控制器类
//2.制作控制器类,等同于Servlet
//2.1必须是一个spring管理的bean
//2.2定义具体处理请求的方法
//2.3设置当前方法的访问路径
//2.4设置响应结果为json数据
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save(){
System.out.println("user save ...");
return "{'module':'springmvc'}";
}
}
创建配置类
//3.定义配置类加载Controller对应的bean
@Configuration
@ComponentScan("com.itheima.controller")
public class SpringMvcConfig {
}
创建tomcat的servlet容器配置类
public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
//加载springMVC配置sadadas
@Override
protected WebApplicationContext createServletApplicationContext() {
// 初始化WebApplicationContext对象
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
// 加载指定配置类
ctx.register(SpringMvcConfig.class);
return ctx;
}
// 设置tomcat接受的请求哪些归SpringMVC处理
@Override
protected String[] getServletMappings() {
return new String[]{
"/"};
}
// 设置spring相关配置
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
配置tomcat环境
测试
默认会访问webapp下的index.html
测试我们写的路径
常用注解
@Controller
名称 | @Controller |
---|---|
类型 | 类注解 |
位置 | SpringMVC控制器类定义上方 |
作用 | 设定SpringMVC的核心控制器bean |
@RequestMapping
名称 | @RequestMapping |
---|---|
类型 | 类注解或方法注解 |
位置 | SpringMVC控制器类或方法定义上方 |
作用 | 设置当前控制器方法请求访问路径 |
相关属性 | value(默认),请求访问路径 |
@ResponseBody
名称 | @ResponseBody |
---|---|
类型 | 类注解或方法注解 |
位置 | SpringMVC控制器类或方法定义上方 |
作用 | 设置当前控制器方法响应内容为当前返回值,无需解析 |
工作流程解析
为了更好的使用SpringMVC,我们将SpringMVC的使用过程总共分两个阶段来分析,分别是启动服务器初始化过程
和单次请求过程
启动服务器初始化过程
-
服务器启动,执行ServletContainersInitConfig类,初始化web容器
- 功能类似于以前的web.xml
-
执行createServletApplicationContext方法,创建了WebApplicationContext对象
- 该方法加载SpringMVC的配置类SpringMvcConfig来初始化SpringMVC的容器
-
加载SpringMvcConfig配置类
-
执行@ComponentScan加载对应的bean
- 扫描指定包及其子包下所有类上的注解,如Controller类上的@Controller注解
-
加载UserController,每个@RequestMapping的名称对应一个具体的方法
- 此时就建立了
/save
和 save方法的对应关系
- 此时就建立了
-
执行getServletMappings方法,设定SpringMVC拦截请求的路径规则
/
代表所拦截请求的路径规则,只有被拦截后才能交给SpringMVC来处理请求
单次请求过程
- 发送请求
http://localhost/save
- web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
- 解析请求路径/save
- 由/save匹配执行对应的方法save()
- 上面的第五步已经将请求路径和方法建立了对应关系,通过/save就能找到对应的save方法
- 执行save()
- 检测到有@ResponseBody直接将save()方法的返回值作为响应体返回给请求方
SSM整合
整合配置
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>springmvc_08_ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
创建项目包结构
- config目录存放的是相关的配置类
- controller编写的是Controller类
- mapper存放的是mapper接口,因为使用的是Mapper接口代理方式,所以没有实现类包
- service存的是Service接口,impl存放的是Service实现类
- resources:存入的是配置文件,如Jdbc.properties
- webapp:目录可以存放静态资源
- test/java:存放的是测试类
SpringConfig
package com.caq.scw.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@PropertySource("classpath:jdbc.properties")
@ComponentScan({
"com.caq.scw.service","com.caq.scw.mapper"})
@Import({
JdbcConfig.class,MybatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
JdbcConfig
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
MybatisConfig
public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.caq.srw");
ssfb.setDataSource(dataSource);
return ssfb;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.caq.scw.mapper");
return msc;
}
}
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/srw?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root
SpringMVConfig
@Configuration
@ComponentScan("com.caq.scw.controller")
@EnableWebMvc
public class SpringMvcConfig {
}
Web项目入口配置类
package com.caq.scw.config;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
//加载Spring配置类
protected Class<?>[] getRootConfigClasses() {
return new Class[]{
SpringConfig.class};
}
//加载SpringMVC配置类
protected Class<?>[] getServletConfigClasses() {
return new Class[]{
SpringMvcConfig.class};
}
//设置SpringMVC请求地址拦截规则
protected String[] getServletMappings() {
return new String[]{
"/"};
}
}
至此SSM整合的环境就已经搭建好了。
功能模块开发
建表写sql
use srw;
DROP TABLE IF EXISTS t_admin;
CREATE TABLE t_admin
(
id int NOT NULL auto_increment,
login_acct VARCHAR(255) NOT NULL,
user_pswd char(32) NOT NUll,
user_name VARCHAR(255) NOT NUll,
email VARCHAR(255) NOT NUll,
create_time char(19),
PRIMARY KEY(id)
)
插入数据直接在表里加了,不写sql了
实体类
@Data
@ToString
public class TAdmin {
private Integer id;
private String login_acct;
private String user_pswd;
private String user_name;
private String email;
private String create_time;
}
dao
public interface TAdminMapper {
@Select("select * from t_admin where id= #{id}")
TAdmin selectById(Integer id);
@Update("update t_admin set user_name = #{new_name} where id = #{id}")
boolean updateById(@Param("new_name") String name,@Param("id") Integer id);
//下面都是mbg工程自动生成的
long countByExample(TAdminExample example);
int deleteByExample(TAdminExample example);
int deleteByPrimaryKey(Integer id);
int insert(TAdmin record);
int insertSelective(TAdmin record);
List<TAdmin> selectByExample(TAdminExample example);
TAdmin selectByPrimaryKey(Integer id);
int updateByExampleSelective(@Param("record") TAdmin record, @Param("example") TAdminExample example);
int updateByExample(@Param("record") TAdmin record, @Param("example") TAdminExample example);
int updateByPrimaryKeySelective(TAdmin record);
int updateByPrimaryKey(TAdmin record);
}
service
@Transactional
public interface AdminService {
TAdmin selectByIdTAdmin(Integer id);
boolean updateById(String name,Integer id);
}
@Service
public class AdminServiceImpl implements AdminService {
@Resource
TAdminMapper tAdminMapper;
@Override
public TAdmin selectByIdTAdmin(Integer id) {
return tAdminMapper.selectById(id);
}
@Override
public boolean updateById(String name, Integer id) {
boolean b = tAdminMapper.updateById(name, id);
int i = 1/0;
boolean s = tAdminMapper.updateById(name, id);
return s;
}
}
controller
@RestController
@RequestMapping("/ssm")
public class UserController {
@Autowired
AdminService adminService;
@GetMapping("/{id}")
public TAdmin save(@PathVariable Integer id){
TAdmin tAdmin = adminService.selectByIdTAdmin(id);
return tAdmin;
}
}
接下来我们就先把业务层的代码使用Spring整合Junit
的知识点进行单元测试:
单元测试
后端测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Test1 {
@Autowired
private AdminService adminService;
@Test
public void testGetById(){
TAdmin tAdmin = adminService.selectByIdTAdmin(1);
System.out.println(tAdmin);
}
}
前端测试
统一结果封装
为啥要封装,因为后端查询数据返回给前端的数据类型不一致
比如我增加,修改返回的是boolean
查询返回的是对象、集合对象,所以前端需要后端返回一个统一的数据结果,前端解析的时候就可以按照一种方式进行解析
如何做
思路如下:
创建结果模型类,封装数据到data属性中
操作成功,封装操作结果到code属性中
操作失败,封装错误信息到message(msg)属性中
根据分析,我们可以设置统一数据返回结果类:
@Data
public class Result {
private Object data;
private Integer code;
private String msg;
public Result() {
}
public Result(Integer code, Object data) {
this.code = code;
this.data = data;
}
public Result(Integer code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
}
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;
public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
}
类名不是固定的,根据需求定义即可
再次测试
@RestController
@RequestMapping("/ssm")
public class UserController {
@Autowired
AdminService adminService;
@GetMapping("/{id}")
public Result save(@PathVariable Integer id){
TAdmin tAdmin = adminService.selectByIdTAdmin(id);
Integer code = tAdmin != null ? Code.GET_OK : Code.GET_ERR;
String msg = tAdmin != null ? "查询成功" : "查询失败";
return new Result(code,tAdmin,msg);
}
}
统一异常处理
为啥要做统一异常处理?
前面说过后端返回给前端的数据要格式统一,如果程序出错了格式我们没用对应的异常处理机制,返回给前端的结果还是乱的!
异常的种类及出现异常的原因
- 框架内部抛出的异常:使用不合规导致
- 数据层抛出的异常:外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
可以看出,异常在我们开发的每一个位置都有可能出现异常,而且它们还是不可避免的~
创建异常处理器类
package com.caq.scw.controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ExceptionAdvice {
//除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
@ExceptionHandler(Exception.class)
public void doException(Exception ex){
System.out.println("用于处理非预期的异常");
}
}
确保SpringMvcConfig能够扫描到异常处理器类
让程序抛出异常
package com.caq.scw.controller;
import com.caq.scw.common.Code;
import com.caq.scw.common.Result;
import com.caq.scw.entity.TAdmin;
import com.caq.scw.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/ssm")
public class UserController {
@Autowired
AdminService adminService;
@GetMapping("/{id}")
public Result save(@PathVariable Integer id) {
int i = 1 / 0;
TAdmin tAdmin = adminService.selectByIdTAdmin(id);
Integer code = tAdmin != null ? Code.GET_OK : Code.GET_ERR;
String msg = tAdmin != null ? "查询成功" : "查询失败";
return new Result(code, tAdmin, msg);
}
}
测试
前端这时候是没有返回数据的,这样也没有做到统一
所以我们让异常处理类能返回结果给前端
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ExceptionAdvice {
//除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
System.out.println("用于处理非预期的异常");
return new Result(20001,null,"后端出现了非预期异常");
}
}
至此,就算后台执行的过程中抛出异常,最终也能按照我们和前端约定好的格式返回给前端。
新知识点
@RestControllerAdvice
名称 | @RestControllerAdvice |
---|---|
类型 | 类注解 |
位置 | Rest风格开发的控制器增强类定义上方 |
作用 | 为Rest风格开发的控制器类做增强 |
**说明:**此注解自带@ResponseBody注解与@Component注解,具备对应的功能
@ExceptionHandler
名称 | @ExceptionHandler |
---|---|
类型 | 方法注解 |
位置 | 专用于异常处理的控制器方法上方 |
作用 | 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行 |
**说明:**此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
异常处理方案
异常分类
异常的种类又很多种,如果每一个异常都对应一个@ExceptionHandler,那得写多少个方法来处理各自的异常,所以我们在处理异常之前,需要对异常进行一个分类:
-
业务异常(BusinessException)
- 数据格式问题,比如在年龄栏输入字符串
-
系统异常
- 项目运行过程中可预计但无法避免的异常
- 服务器宕机
- 项目运行过程中可预计但无法避免的异常
-
其他异常
- 查找不到文件,文件丢失,位置更改
异常解决方案
- 业务异常(BusinessException)
- 发生消息给用户
- 常见的用户名或密码错误
- 发生消息给用户
- 系统异常
- 发生固定消息传递给用户
- 系统繁忙
- 系统正在维护升级,请稍后再试
- 系统出问题,请联系系统管理员等
- 发送特定消息给运维人员,提醒维护
- 可以发送短信、邮箱或者是公司内部通信软件
- 记录日志
- 发消息和记录日志对用户来说是不可见的,属于后台程序
- 发生固定消息传递给用户
- 其他异常(Exception)
- 发送固定消息传递给用户,安抚用户
- 发送特定消息给编程人员,提醒维护(纳入预期范围内)
- 一般是程序没有考虑全,比如未做非空校验等
- 记录日志
具体实现
思路:
1.先通过自定义异常,完成BusinessException和SystemException的定义
2.将其他异常包装成自定义异常类型
3.在异常处理器类中对不同的异常进行处理
自定义异常类
//自定义异常处理器,用于封装异常信息,对异常进行分类
public class SystemException extends RuntimeException{
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public SystemException(Integer code, String message) {
super(message);
this.code = code;
}
public SystemException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
//自定义异常处理器,用于封装异常信息,对异常进行分类
public class BusinessException extends RuntimeException{
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
说明:
- 让自定义异常类继承
RuntimeException
的好处是,后期在抛出这两个异常的时候,就不用在try…catch…或throws了 - 自定义异常类中添加
code
属性的原因是为了更好的区分异常是来自哪个业务的
将其他异常包成自定义异常
@RestController
@RequestMapping("/ssm")
public class UserController {
@Autowired
AdminService adminService;
@GetMapping("/{id}")
public Result save(@PathVariable Integer id) {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试",e);
}
TAdmin tAdmin = adminService.selectByIdTAdmin(id);
Integer code = tAdmin != null ? Code.GET_OK : Code.GET_ERR;
String msg = tAdmin != null ? "查询成功" : "查询失败";
return new Result(code, tAdmin, msg);
}
}
具体的包装方式有:
- 方式一:
try{}catch(){}
在catch中重新throw我们自定义异常即可。 - 方式二:直接throw自定义异常即可
上面为了使code
看着更专业些,我们在Code类中再新增需要的属性
//状态码
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;
public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
public static final Integer SYSTEM_ERR = 50001;
public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
public static final Integer SYSTEM_UNKNOW_ERR = 59999;
public static final Integer BUSINESS_ERR = 60002;
}
处理器类中处理自定义异常
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
//@ExceptionHandler用于设置当前处理器类对应的异常类型
@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(ex.getCode(),null,ex.getMessage());
}
@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException ex){
return new Result(ex.getCode(),null,ex.getMessage());
}
//除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
@ExceptionHandler(Exception.class)
public Result doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(Code.SYSTEM_UNKNOW_ERR,null,"系统繁忙,请稍后再试!");
}
}
测试
至此完成,统一的异常处理方案。不管后台哪一层抛出异常,都会以我们与前端约定好的方式进行返回,前端只需要把信息获取到,根据返回的正确与否来展示不同的内容即可。
实战
将静态资源放到webui模块下的webapp目录下
因为添加了静态资源,SpringMVC会拦截,所有需要在SpringConfig的配置类中将静态资源进行放行。
SpringMvcSupport
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将/pages下的所有请求指向/pages文件夹,下面同理bootstrap
// registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
registry.addResourceHandler("/bootstrap/**").addResourceLocations("/bootstrap/");
registry.addResourceHandler("/fonts/**").addResourceLocations("/fonts/");
registry.addResourceHandler("/img/**").addResourceLocations("/img/");
registry.addResourceHandler("/jquery/**").addResourceLocations("/jquery/");
registry.addResourceHandler("/script/**").addResourceLocations("/script/");
registry.addResourceHandler("/ztree/**").addResourceLocations("/ztree/");
registry.addResourceHandler("*.html").addResourceLocations("./");
// registry.addResourceHandler("*.jsp").addResourceLocations("./");
}
}
扫描SpringMvcSupport
@Configuration
@ComponentScan({
"com.caq.scw.controller","com.caq.scw.exception","com.caq.scw.config"})
@EnableWebMvc
public class SpringMvcConfig {
}
测试
资源已经能正常访问了
拦截器
拦截器概念
了解拦截器的概念之前我们先来看这张图
(1)浏览器发送一个请求会先到Tomcat的web服务器
(2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3)如果是静态资源,会直接到Tomcat的项目部署目录下去直接访问
(4)如果是动态资源,就需要交给项目的后台代码进行处理
(5)在找到具体的方法之前,我们可以去配置过滤器(可以配置多个),按照顺序进行执行
(6)然后进入到到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
(7)如果满足规则,则进行处理,找到其对应的controller类中的方法进行执行,完成后返回结果
(8)如果不满足规则,则不进行处理
(9)这个时候,如果我们需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
这个就是拦截器要做的事。
- 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
- 作用:
- 在指定的方法调用前后执行预先设定的代码
- 阻止原始方法的执行
- 总结:拦截器就是用来做增强
看完以后,大家会发现
- 拦截器和过滤器在作用和执行顺序上也很相似
所以这个时候,就有一个问题需要思考:拦截器和过滤器之间的区别是什么?
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
入门案例
创建拦截器类
package com.caq.scw.config;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
//定义拦截器类,实现HandlerInterceptor接口
//注意当前类必须受Spring容器控制
public class ProjectInterceptor implements HandlerInterceptor {
@Override
//原始方法调用前执行的内容
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}
@Override
//原始方法调用后执行的内容
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
@Override
//原始方法调用完成后执行的内容
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
**注意:**拦截器类要被SpringMVC容器扫描到。
配置拦截器类
registry.addInterceptor(projectInterceptor).addPathPatterns(“/pages”,“/pages/*”);
通过这里来指定拦截的路径
package com.caq.scw.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将/pages下的所有请求指向/pages文件夹,下面同理bootstrap
// registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
registry.addResourceHandler("/bootstrap/**").addResourceLocations("/bootstrap/");
registry.addResourceHandler("/fonts/**").addResourceLocations("/fonts/");
registry.addResourceHandler("/img/**").addResourceLocations("/img/");
registry.addResourceHandler("/jquery/**").addResourceLocations("/jquery/");
registry.addResourceHandler("/script/**").addResourceLocations("/script/");
registry.addResourceHandler("/ztree/**").addResourceLocations("/ztree/");
registry.addResourceHandler("*.html").addResourceLocations("./");
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
// registry.addResourceHandler("*.jsp").addResourceLocations("./");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置多拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/pages","/pages/*");
}
}
测试
拦截器参数
前置处理方法
原始方法之前运行preHandle
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
- request:请求对象
- response:响应对象
- handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装
使用request对象可以获取请求数据中的内容,如获取请求头的Content-Type
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String contentType = request.getHeader("Content-Type");
System.out.println("preHandle..."+contentType);
return true;
}
使用handler参数,可以获取方法的相关信息
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod hm = (HandlerMethod)handler;
String methodName = hm.getMethod().getName();//可以获取方法的名称
System.out.println("preHandle..."+methodName);
return true;
}
后置处理方法
原始方法运行后运行,如果原始方法被拦截,则不执行
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
前三个参数和上面的是一致的。
modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
因为咱们现在都是返回json数据,所以该参数的使用率不高。
完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("afterCompletion");
}
前三个参数与上面的是一致的。
ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
因为我们现在已经有全局异常处理器类,所以该参数的使用率也不高。
这三个方法中,最常用的是preHandle,在这个方法中可以通过返回值来决定是否要进行放行,我们可以把业务逻辑放在该方法中,如果满足业务则返回true放行,不满足则返回false拦截。
拦截器工作流程分析
当有拦截器后,请求会先进入preHandle方法,
如果方法返回true,则放行继续执行后面的handle[controller的方法]和后面的方法
如果返回false,则直接跳过后面方法的执行。
配置多个拦截器
目前,我们在项目中只添加了一个拦截器,如果有多个,该如何配置?配置多个后,执行顺序是什么?
创建拦截器类
同上我们可以复制第一个拦截器进行修改即可
配置拦截器类
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;
@Autowired
private ProjectInterceptor2 projectInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置多拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/pages","/pages/*");
registry.addInterceptor(projectInterceptor2).addPathPatterns("/pages","/pages/*");
}
}
测试
拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。
- 当配置多个拦截器时,形成拦截器链
- 拦截器链的运行顺序参照拦截器添加顺序为准
- 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
- 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行。
这个顺序不太好记,最终只需要把握住一个原则即可:以最终的运行结果为准