• 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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'}";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建配置类

    //3.定义配置类加载Controller对应的bean
    @Configuration
    @ComponentScan("com.itheima.controller")
    public class SpringMvcConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    创建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;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    配置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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    创建项目包结构

    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 {
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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
    
    • 1
    • 2
    • 3
    • 4

    SpringMVConfig

    @Configuration
    @ComponentScan("com.caq.scw.controller")
    @EnableWebMvc
    public class SpringMvcConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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[]{"/"};
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    至此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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实体类

    @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    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;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    接下来我们就先把业务层的代码使用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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    类名不是固定的,根据需求定义即可

    再次测试

    @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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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("用于处理非预期的异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    确保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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    测试

    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,"后端出现了非预期异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    说明:

    • 让自定义异常类继承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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    具体的包装方式有:

    • 方式一: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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    处理器类中处理自定义异常

    //@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,"系统繁忙,请稍后再试!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    测试

    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("./");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    扫描SpringMvcSupport

    @Configuration
    @ComponentScan({"com.caq.scw.controller","com.caq.scw.exception","com.caq.scw.config"})
    @EnableWebMvc
    public class SpringMvcConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试

    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...");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    **注意:**拦截器类要被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/*");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    测试

    image-20220623105325272

    拦截器参数

    前置处理方法

    原始方法之前运行preHandle

    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    后置处理方法

    原始方法运行后运行,如果原始方法被拦截,则不执行

    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前三个参数和上面的是一致的。

    modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整

    因为咱们现在都是返回json数据,所以该参数的使用率不高。

    完成处理方法

    拦截器最后执行的方法,无论原始方法是否执行

    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前三个参数与上面的是一致的。

    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/*");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    测试

    image-20220623110728735

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

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

    image-20220623110853528

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

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

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

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

  • 相关阅读:
    QT Creator 正则替换功能
    在群晖NAS上搭建导航页_通过Web Station搭建
    Android-源码分析-MTK平台BUG解决:客户电池NTC功能(移植高低温报警,关机报警功能)---第三天分析与解决(已解决)
    【经验总结】Ubuntu 源代码方式安装 Microsoft DeepSpeed
    基于SpringBoot使用MyBatisX插件
    Go语言为任意类型添加方法
    ubuntu安装依赖包时显示需要先安装其所需要的各种安装包)apt-get源有问题
    数据结构与算法-顺序表
    【 构建maven工程时,配置了阿里云的前提下,依旧使用中央仓库下载依赖导致失败的问题!】
    流量染色SDK设计的思考
  • 原文地址:https://blog.csdn.net/qq_45714272/article/details/125434848