SpringMVC快速入门(2022优化版)

概述

学习SpringMVC我们先来回顾下现在web程序是如何做的,咱们现在web程序大都基于三层架构来实现。

三层架构

image-20220623111153961

  • 浏览器发送一个请求给后端服务器,后端服务器现在是使用Servlet来接收请求和数据

  • 如果所有的处理都交给Servlet来处理的话,所有的东西都耦合在一起,对后期的维护和扩展极为不利

  • 将后端服务器Servlet拆分成三层,分别是webservicedao

    • web层主要由servlet来处理,负责页面请求和数据的收集以及响应结果给前端
    • service层主要负责业务逻辑的处理
    • dao层主要负责数据的增删改查操作
  • servlet处理请求和数据的时候,存在的问题是一个servlet只能处理一个请求

  • 针对web层进行了优化,采用了MVC设计模式,将其设计为controllerviewModel

    • controller负责请求和数据的接收,接收后将其转发给service进行业务处理
    • service根据需要会调用dao对数据进行增删改查
    • dao把数据处理完后将结果交给service,service再交给controller
    • controller根据需求组装成Model和View,Model和View组合起来生成页面转发给前端浏览器
    • 这样做的好处就是controller可以处理多个请求,并对请求进行分发,执行不同的业务操作。

随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不是需求,所以异步调用慢慢的走到了前台,是现在比较流行的一种处理方式。

image-20220623111246637

  • 因为是异步调用,所以后端不需要返回view视图,将其去除
  • 前端如果通过异步调用的方式进行交互,后台就需要将返回的数据转换成json格式进行返回
  • SpringMVC主要负责的就是
    • controller如何接收请求和数据
    • 如何将请求和数据转发给业务层
    • 如何将响应数据转换成json发回到前端

介绍了这么多,对SpringMVC进行一个定义

  • SpringMVC是一种基于Java实现MVC模型的轻量级Web框架

  • 优点

    • 使用简单、开发便捷(相比于Servlet)
    • 灵活性强

小总结

SpringMVC属于Spring,是Spring的一部分

SpringMVC是用来和Servlet技术功能等同,均属于web层或者表现层开发技术

SpringMVC与Servlet相比,开发起来更简单快捷,能用更少的代码完成表现层代码的开发

image-20220119121539011

入门

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环境

image-20220620230645442

测试

默认会访问webapp下的index.html

image-20220620231321104

测试我们写的路径

image-20220620231428922

常用注解

@Controller

名称 @Controller
类型 类注解
位置 SpringMVC控制器类定义上方
作用 设定SpringMVC的核心控制器bean

@RequestMapping

名称 @RequestMapping
类型 类注解或方法注解
位置 SpringMVC控制器类或方法定义上方
作用 设置当前控制器方法请求访问路径
相关属性 value(默认),请求访问路径

@ResponseBody

名称 @ResponseBody
类型 类注解或方法注解
位置 SpringMVC控制器类或方法定义上方
作用 设置当前控制器方法响应内容为当前返回值,无需解析

工作流程解析

为了更好的使用SpringMVC,我们将SpringMVC的使用过程总共分两个阶段来分析,分别是启动服务器初始化过程单次请求过程

image-20220623111834619

启动服务器初始化过程

  1. 服务器启动,执行ServletContainersInitConfig类,初始化web容器

    • 功能类似于以前的web.xml
  2. 执行createServletApplicationContext方法,创建了WebApplicationContext对象

    • 该方法加载SpringMVC的配置类SpringMvcConfig来初始化SpringMVC的容器
  3. 加载SpringMvcConfig配置类

    1630433335744

  4. 执行@ComponentScan加载对应的bean

    • 扫描指定包及其子包下所有类上的注解,如Controller类上的@Controller注解
  5. 加载UserController,每个@RequestMapping的名称对应一个具体的方法

    1630433398932

    • 此时就建立了 /save 和 save方法的对应关系
  6. 执行getServletMappings方法,设定SpringMVC拦截请求的路径规则

    1630433510528

    • /代表所拦截请求的路径规则,只有被拦截后才能交给SpringMVC来处理请求

单次请求过程

  1. 发送请求http://localhost/save
  2. web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
  3. 解析请求路径/save
  4. 由/save匹配执行对应的方法save()
    • 上面的第五步已经将请求路径和方法建立了对应关系,通过/save就能找到对应的save方法
  5. 执行save()
  6. 检测到有@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>

创建项目包结构

image-20220621204932977

  • 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);
    }

}

image-20220622112240143

前端测试

统一结果封装

为啥要封装,因为后端查询数据返回给前端的数据类型不一致

比如我增加,修改返回的是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);
    }
}

image-20220622120956364

统一异常处理

为啥要做统一异常处理?

前面说过后端返回给前端的数据要格式统一,如果程序出错了格式我们没用对应的异常处理机制,返回给前端的结果还是乱的!

异常的种类及出现异常的原因

  • 框架内部抛出的异常:使用不合规导致
  • 数据层抛出的异常:外部服务器故障导致(例如:服务器访问超时)
  • 业务层抛出的异常:业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
  • 表现层抛出的异常:数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
  • 工具类抛出的异常:工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)

可以看出,异常在我们开发的每一个位置都有可能出现异常,而且它们还是不可避免的~

创建异常处理器类

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);
    }
}

测试

image-20220622184022542

前端这时候是没有返回数据的,这样也没有做到统一

image-20220622184308173

所以我们让异常处理类能返回结果给前端

//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ExceptionAdvice {
    
    

    //除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex){
    
    
        System.out.println("用于处理非预期的异常");
        return new Result(20001,null,"后端出现了非预期异常");
    }
}

image-20220622184422115

至此,就算后台执行的过程中抛出异常,最终也能按照我们和前端约定好的格式返回给前端。

新知识点

@RestControllerAdvice

名称 @RestControllerAdvice
类型 类注解
位置 Rest风格开发的控制器增强类定义上方
作用 为Rest风格开发的控制器类做增强

**说明:**此注解自带@ResponseBody注解与@Component注解,具备对应的功能

image-20220622184601917

@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,"系统繁忙,请稍后再试!");
    }
}

测试

image-20220622191943471

至此完成,统一的异常处理方案。不管后台哪一层抛出异常,都会以我们与前端约定好的方式进行返回,前端只需要把信息获取到,根据返回的正确与否来展示不同的内容即可。

image-20220622192104240

实战

将静态资源放到webui模块下的webapp目录下

image-20220622192617974

因为添加了静态资源,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 {
    
    
}

测试

image-20220622194811655

资源已经能正常访问了

拦截器

拦截器概念

了解拦截器的概念之前我们先来看这张图

image-20220622213359540

(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的访问进行增强

image-20220622224633490

入门案例

创建拦截器类

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容器扫描到。

image-20220623105102626

配置拦截器类

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/*");
    }

}

测试

image-20220623105325272

拦截器参数

前置处理方法

原始方法之前运行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拦截。

拦截器工作流程分析

image-20220623105439046

当有拦截器后,请求会先进入preHandle方法,

​ 如果方法返回true,则放行继续执行后面的handle[controller的方法]和后面的方法

​ 如果返回false,则直接跳过后面方法的执行。

配置多个拦截器

目前,我们在项目中只添加了一个拦截器,如果有多个,该如何配置?配置多个后,执行顺序是什么?

创建拦截器类

同上我们可以复制第一个拦截器进行修改即可

image-20220623110032102

配置拦截器类

@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/*");
    }

}

测试

image-20220623110728735

拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。

  • 当配置多个拦截器时,形成拦截器链
  • 拦截器链的运行顺序参照拦截器添加顺序为准
  • 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
  • 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作

image-20220623110853528

preHandle:与配置顺序相同,必定运行

postHandle:与配置顺序相反,可能不运行

afterCompletion:与配置顺序相反,可能不运行。

这个顺序不太好记,最终只需要把握住一个原则即可:以最终的运行结果为准

猜你喜欢

转载自blog.csdn.net/qq_45714272/article/details/125434848