SSM整合

流程分析

  1. 创建工程

    • 创建一个Maven的web工程
    • pom.xml添加SSM需要的依赖jar包
    • 编写Web项目的入口配置类,实现AbstractAnnotationConfigDispatcherServletInitializer接口并重写以下方法

      • getRootConfigClasses() :返回Spring的配置类 —> 需要SpringConfig配置类
      • getServletConfigClasses() :返回SpringMVC的配置类 —> 需要SpringMvcConfig配置类
      • getServletMappings() : 设置SpringMVC请求拦截路径规则
      • getServletFilters() :设置过滤器,解决POST请求中文乱码问题
  2. 整合SSM中的各种配置

    • Spring:
      • 标识该类为配置类,使用@Configuration
      • 扫描Service所在的包,使用@ComponentScan
      • Service层要管理事务,使用@EnableTransactionManagement
      • 读取外部的properties配置文件,使用@PropertySource
    • Mybatis:

      • 第三方数据源配置类 JdbcConfig
      • 构建DataSource数据源,使用@Bean来将返回的第三方类注册为Bean,使用@Value来注入数据源属性
      • 构建平台事务管理器,DataSourceTransactionManager,使用@Bean
      • Mybatis配置类 MybatisConfig
      • 构建SqlSessionFactoryBean并设置别名扫描与数据源,使用@Bean
      • 构建MapperScannerConfigurer并设置DAO层的包扫描
    • SpringMvcConfig

      • 标识该类为配置类,使用@Configuration
      • 扫描Controller所在的包,使用@ComponentScan
      • 开启SpringMVC注解支持,使用@EnableWebMvc
  3. 功能模块(与具体的业务模块有关)

    • 创建数据库表

    • 根据数据库表创建对应的模型类

    • 通过Dao层完成数据库表的增删改查(接口+自动代理)

    • 编写

      1
      Service

      层(Service接口+实现类)

      • @Service
      • @Transactional
      • 整合Junit对业务层进行单元测试
        • @RunWith
        • @ContextConfiguration
        • @Test
    • 编写

      1
      Controller

      • 接收请求 @RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping
      • 接收数据 简单、POJO、嵌套POJO、集合、数组、JSON数据类型
        • @RequestParam
        • @PathVariable
        • @RequestBody
      • 转发业务层
        • @Autowired
      • 响应结果
        • @ResponseBody

整合配置

  • 步骤一:创建Maven的web项目

  • 步骤二:导入坐标
    导入的坐标有:Springmvc,jdbc,mybatis,mysql-connector-java,druid,junit,javax.servlet-api,jackson-databind

    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
    <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.46</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>
  • 步骤三:创建项目包结构

    • com.blog.config目录存放的是相关的配置类
    • com.blog.controller编写的是Controller类
    • com.blog.dao存放的是Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包
    • com.blog.service存的是Service接口,com.blog.service.impl存放的是Service实现类
    • resources:存入的是配置文件,如Jdbc.properties
    • webapp:目录可以存放静态资源
    • test/java:存放的是测试类
  • 步骤四:创建jdbc.properties

    1
    2
    3
    4
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:13306/ssm_db?useSSL=false
    jdbc.username=root
    jdbc.password=PASSWORD.
  • 步骤五:创建JdbcConfig配置类

    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
    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 dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dataSource){
    DataSourceTransactionManager ds = new DataSourceTransactionManager();
    ds.setDataSource(dataSource);
    return ds;
    }
    }
  • 步骤六:创建MyBatisConfig配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyBatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    factoryBean.setTypeAliasesPackage("com.blog.domain");
    return factoryBean;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    MapperScannerConfigurer msc = new MapperScannerConfigurer();
    msc.setBasePackage("com.blog.dao");
    return msc;
    }
    }
  • 步骤七:创建SpringConfig配置类

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    @ComponentScan("com.blog.service")
    @PropertySource("jdbc.properties")
    @EnableTransactionManagement
    @Import({JdbcConfig.class, MyBatisConfig.class})
    public class SpringConfig {
    }
  • 步骤八:创建SpringMvcConfig配置类

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("com.blog.controller")
    @EnableWebMvc
    public class SpringMvcConfig {
    }
  • 步骤九:创建ServletContainersInitConfig配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    protected Class<?>[] getRootConfigClasses() {
    return new Class[]{SpringConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
    return new Class[]{SpringMvcConfig.class};
    }

    protected String[] getServletMappings() {
    return new String[]{"/"};
    }

    @Override
    protected Filter[] getServletFilters() {
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding("utf-8");
    return new Filter[]{filter};
    }
    }

功能模块开发

需求:对表tbl_book进行新增、修改、删除、根据ID查询和查询所有

  • 步骤一:

    创建数据库及表

    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
    create database ssm_db;
    use ssm_db;
    create table tbl_book
    (
    id int primary key auto_increment,
    type varchar(20),
    `name` varchar(50),
    description varchar(255)
    );

    insert into `tbl_book`(`id`, `type`, `name`, `description`)
    values (1, '计算机理论', 'Spring实战 第五版', 'Spring入门经典教程,深入理解Spring原理技术内幕'),
    (2, '计算机理论', 'Spring 5核心原理与30个类手写实践', '十年沉淀之作,手写Spring精华思想'),
    (3, '计算机理论', 'Spring 5设计模式', '深入Spring源码刨析Spring源码中蕴含的10大设计模式'),
    (4, '计算机理论', 'Spring MVC+Mybatis开发从入门到项目实战',
    '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'),
    (5, '计算机理论', '轻量级Java Web企业应用实战', '源码级刨析Spring框架,适合已掌握Java基础的读者'),
    (6, '计算机理论', 'Java核心技术 卷Ⅰ 基础知识(原书第11版)',
    'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新'),
    (7, '计算机理论', '深入理解Java虚拟机', '5个纬度全面刨析JVM,大厂面试知识点全覆盖'),
    (8, '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉'),
    (9, '计算机理论', '零基础学Java(全彩版)', '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术'),
    (10, '市场营销', '直播就这么做:主播高效沟通实战指南', '李子柒、李佳奇、薇娅成长为网红的秘密都在书中'),
    (11, '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'),
    (12, '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');
  • 步骤二:编写模型类

    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
    public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

    public String getType() {
    return type;
    }

    public void setType(String type) {
    this.type = type;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getDescription() {
    return description;
    }

    public void setDescription(String description) {
    this.description = description;
    }

    @Override
    public String toString() {
    return "Book{" +
    "id=" + id +
    ", type='" + type + '\'' +
    ", name='" + name + '\'' +
    ", description='" + description + '\'' +
    '}';
    }
    }
  • 步骤三:编写Dao接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface BookDao {
    @Insert("insert into tbl_book values (null, #{type}, #{name}, #{description})")
    void save(Book book);

    @Update("update tbl_book set type=#{type}, `name`=#{name}, `description`=#{description} where id=#{id}")
    void update(Book book);

    @Delete("delete from tbl_book where id=#{id}")
    void delete(Integer id);

    @Select("select * from tbl_book where id=#{id}")
    void getById(Integer id);

    @Select("select * from tbl_book")
    void getAll();
    }
  • 步骤四:编写Service接口及其实现类

    • BookService
    • BookServiceImpl
    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
    @Transactional
    public interface BookService {
    /**
    * 保存
    * @param book
    * @return
    */
    boolean save(Book book);

    /**
    * 修改
    * @param book
    * @return
    */
    boolean update(Book book);

    /**
    * 按id删除
    * @param id
    * @return
    */
    boolean delete(Integer id);

    /**
    * 按id查询
    * @param id
    * @return
    */
    Book getById(Integer id);

    /**
    * 查询所有
    * @return
    */
    List<Book> getAll();
    }
  • ```
    步骤五:

    1
    2
    3

    编写Controller类

    @RestController
    @RequestMapping(“/books”)
    public class BookController {

    @Autowired
    private BookService bookService;
    
    @PostMapping
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }
    
    @PutMapping
    public boolean update(@RequestBody Book book) {
        return bookService.update(book);
    }
    
    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable Integer id) {
        return bookService.delete(id);
    }
    
    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }
    
    @GetMapping
    public List<Book> getAll() {
        return bookService.getAll();
    }
    

    }

    1
    2
    3
    4
    5

    ### 单元测试

    - ```
    步骤一:

    新建测试类

    1
    2
    3
    4
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class BookServiceTest {
    }
  • ```
    步骤二:

    1
    2
    3

    注入Service

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class BookServiceTest {

    @Autowired
    private BookService bookService;
    

    }

    1
    2
    3

    - ```
    步骤三:

    编写测试方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class BookServiceTest {

    @Autowired
    private BookService bookService;

    @Test
    public void testGetById() {
    Book book = bookService.getById(1);
    System.out.println(book);
    }

    @Test
    public void testGetAll() {
    for (Book book : bookService.getAll()) {
    System.out.println(book);
    }
    }
    }

    运行测试方法,可以查询到对应的数据

PostMan测试

  • 新增
    发送POST请求与数据,访问localhost:8080/books

    1
    2
    3
    4
    5
    {
    "type":"类别测试数据",
    "name":"书名测试数据",
    "description":"描述测试数据"
    }

    数据库中能看到新增的数据

  • 修改
    发送PUT请求与数据,访问localhost:8080/books

    1
    2
    3
    4
    5
    6
    {
    "id":"13",
    "type":"类别测试数据",
    "name":"书名测试数据9527",
    "description":"描述测试数据"
    }

    数据库中能看到修改后的数据

  • 删除
    发送DELETE请求,访问localhost:8080/books/13
    数据库中能看到id为13的数据不见了

  • 查询单个
    发送GET请求,访问localhost:8080/books/1
    可以查询到ID为1的数据,在PostMan中表现为JSON数据

    1
    2
    3
    4
    5
    6
    {
    "id": 1,
    "type": "计算机理论",
    "name": "Spring实战 第五版",
    "description": "Spring入门经典教程,深入理解Spring原理技术内幕"
    }
  • 查询所有
    发送GET请求,访问localhost:8080/books
    PostMan中以JSON对象数组的形式显示了数据库中的所有数据

统一结果封装

表现层与前端数据传输协议定义

SSM整合以及功能模块开发完成后,接下来我们在上述案例的基础上,分析一下有哪些问题需要我们解决。
首先第一个问题是:

  • 在Controller层增删改操作完成后,返回给前端的是boolean类型的数据

    true

  • 在Controller层查询单个,返回给前端的是对象

    1
    2
    3
    4
    5
    6
    {
    "id": 1,
    "type": "计算机理论",
    "name": "Spring实战 第五版",
    "description": "Spring入门经典教程,深入理解Spring原理技术内幕"
    }
  • 在Controller层查询所有,返回给前端的是集合对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [
    {
    "id": 1,
    "type": "计算机理论",
    "name": "Spring实战 第五版",
    "description": "Spring入门经典教程,深入理解Spring原理技术内幕"
    },
    {
    "id": 2,
    "type": "计算机理论",
    "name": "Spring 5核心原理与30个类手写实践",
    "description": "十年沉淀之作,手写Spring精华思想"
    },
    ...
    ]
  • 目前我们就已经有三种数据类型返回给前端了,随着业务的增长,我们需要返回的数据类型就会越来越多。那么前端开发人员在解析数据的时候就比较凌乱了,所以对于前端来说,如果后端能返回一个统一的数据结果,前端在解析的时候就可以按照一种方式进行解析,开发就会变得更加简单

  • 所以现在我们需要解决的问题就是如何将返回的结果数据进行统一,具体如何来做,大体思路如下

    • 为了

      1
      封装返回的结果数据

      :创建结果模型类,封装数据到

      1
      data

      属性中

      • 我们可以设置data的数据类型为Object,这样data中就可以放任意的结果类型了,包括但不限于上面的boolean对象集合对象
    • 为了封装返回的数据是

      1
      何种操作

      ,以及是否

      1
      操作成功

      :封装操作结果到

      1
      code

      属性中

      • 例如增删改操作返回的都是true,那我们怎么分辨这个true到底是还是还是呢?我们就通过这个code来区分
    • 操作失败后,需要封装

      1
      返回错误信息

      提示给用户:封装特殊消息到message(msg)属性中

      • 例如查询或删除的目标不存在,会返回null,那么此时我们需要提示查询/删除的目标不存在,请重试!
  • 那么之前的三种返回方式就可以变为如下形式

    • boolean
    • 对象
    • 对象集合

    规则可以自己定
    这里前三位是固定的
    第四位表示不同的操作
    末位表示成功/失败,1成功,0失败

    1
    2
    3
    4
    {
    "code":20011,
    "data":true
    }
  • 根据分析,我们可以设置统一数据返回结果类

    1
    2
    3
    4
    5
    public class Result{
    private Object data;
    private Integer code;
    private String msg;
    }

    注意:Result类名及类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操作。

表现层与前端数据传输协议实现

前面我们已经分析了如何封装返回结果数据,现在我们来具体实现一下

对于结果封装,我们应该是在表现层进行处理,所以我们把结果类放在controller包下,当然你也可以放在domain包,这个都是可以的,具体如何实现结果封装,具体的步骤如下

  • ```
    步骤一:

    1
    2
    3

    创建Result

    public class Result {

    //描述统一格式中的编码,用于区分操作,可以简化配置0或1表示成功失败
    private Integer code;
    //描述统一格式中的数据
    private Object data;
    //描述统一格式中的消息,可选属性
    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 Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    public Object getData() {
        return data;
    }
    
    public void setData(Object data) {
        this.data = data;
    }
    
    public String getMsg() {
        return msg;
    }
    
    public void setMsg(String msg) {
        this.msg = msg;
    }
    
    @Override
    public String toString() {
        return "Result{" +
                "code=" + code +
                ", data=" + data +
                ", msg='" + msg + '\'' +
                '}';
    }
    

    }

    1
    2
    3

    - `步骤二:`定义返回码Code类

    public class Code {

    public static final Integer SAVE_OK = 20011;
    public static final Integer UPDATE_OK = 20021;
    public static final Integer DELETE_OK = 20031;
    public static final Integer GET_OK = 20041;
    
    public static final Integer SAVE_ERR = 20010;
    public static final Integer UPDATE_ERR = 20020;
    public static final Integer DELETE_ERR = 20030;
    public static final Integer GET_ERR = 20040;
    

    }

    1
    2
    3
    4
    5

    注意:code类中的常量设计也不是固定的,可以根据需要自行增减,例如将查询再进行细分为`GET_OK`,`GET_ALL_OK`,`GET_PAGE_OK`等。

    - `步骤三:`修改Controller类的返回值

    @RestController
    @RequestMapping(“/books”)
    public class BookController {

    @Autowired
    private BookService bookService;
    
    @PostMapping
    public Result save(@RequestBody Book book) {
        boolean flag = bookService.save(book);
        return new Result(flag ? Code.SAVE_OK : Code.SAVE_ERR, flag);
    }
    
    @PutMapping
    public Result update(@RequestBody Book book) {
        boolean flag = bookService.update(book);
        return new Result(flag ? Code.UPDATE_OK : Code.UPDATE_ERR, flag);
    }
    
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        boolean flag = bookService.delete(id);
        return new Result(flag ? Code.DELETE_OK : Code.DELETE_ERR, flag);
    }
    
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
        Book book = bookService.getById(id);
        Integer code = book == null ? Code.GET_ERR : Code.GET_OK;
        String msg = book == null ? "数据查询失败,请重试!" : "";
        return new Result(code, book, msg);
    }
    
    @GetMapping
    public Result getAll() {
        List<Book> bookList = bookService.getAll();
        Integer code = bookList == null ? Code.GET_ERR : Code.GET_OK;
        String msg = bookList == null ? "数据查询失败,请重试!" : "";
        return new Result(code, bookList, msg);
    }
    

    }

    1
    2
    3

    - ```
    步骤四:

    启动服务测试

    五个方法的测试结果如下

    • save
    • update
    • delete
    • getById
    • getAll
    1
    2
    3
    4
    5
    {
    "code": 20011,
    "data": true,
    "msg": null
    }

统一异常处理

问题描述

在学习这部分知识之前,我们先来演示一个效果,修改BookControllergetById()方法,手写一个异常

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
//当id为1的时候,手动添加了一个错误信息
if (id == 1){
int a = 1 / 0;
}
Book book = bookService.getById(id);
Integer code = book == null ? Code.GET_ERR : Code.GET_OK;
String msg = book == null ? "数据查询失败,请重试!" : "";
return new Result(code, book, msg);
}

重新启动服务器,使用PostMan发送请求,当传入的id为1时,会出现如下效果
img
前端接收到这个信息后,和我们之前约定的格式不一致,怎么解决呢?
在解决问题之前,我们先来看一下异常的种类,以及出现异常的原因:

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

了解完上面这些出现异常的位置,我们发现,在我们开发的任何一个位置都可能会出现异常,而且这些异常是不能避免的,所以我们就需要对这些异常来进行处理

1
思考
  1. 各个层级均出现异常,那么异常处理代码要写在哪一层?
    • 所有的异常均抛出到表现层进行处理
  2. 异常的种类很多,表现层如何将所有的异常都处理到呢?
    • 异常分类
  3. 表现层处理异常,每个方法中单独书写,代码书写两巨大,且意义不强,如何解决呢?
    • AOP

对于上面这些问题以及解决方案,SpringMVC已经为我们提供了一套了:

  • 异常处理器:

    • 集中的、统一的处理项目中出现的异常

      1
      2
      3
      4
      5
      6
      7
      @RestControllerAdvice
      public class ProjectExceptionAdvice {
      @ExceptionHandler(Exception.class)
      public Result doException(Exception ex) {
      return new Result(666, null);
      }
      }

异常处理器的使用

  • 步骤一:创建异常处理器类

    1
    2
    3
    4
    5
    6
    7
    @RestControllerAdvice
    public class ProjectExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public void doException(Exception ex) {
    System.out.println("嘿嘿,逮到一个异常~");
    }
    }

    注意:要确保SpringMvcConfig能够扫描到异常处理器类

  • 步骤二:让程序抛出异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
    //当id为1的时候,手动添加了一个错误信息
    if (id == 1){
    int a = 1 / 0;
    }
    Book book = bookService.getById(id);
    Integer code = book == null ? Code.GET_ERR : Code.GET_OK;
    String msg = book == null ? "数据查询失败,请重试!" : "";
    return new Result(code, book, msg);
    }
  • ```
    步骤三:

    1
    2
    3

    使用

    PostMan

    1
    2
    3

    发送

    GET

    1
    2
    3

    请求访问

    localhost:8080/books/1

    1
    2
    3

    控制台输出如下,说明异常已经被拦截,且执行了

    doException()

    1
    2
    3
    4
    5
    6
    7

    方法

    > 嘿嘿,逮到一个异常~

    但是现在没有返回数据给前端,为了统一返回结果,我们继续修改异常处理器类

    @RestControllerAdvice
    public class ProjectExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex) {

      System.out.println("嘿嘿,逮到一个异常~");
      return new Result(666, null, "嘿嘿,逮到一个异常~");
    

    }
    }

    1
    2
    3

    重启服务器,使用PostMan发送请求,此时就能接收到结果了

    {
    “code”: 666,
    “data”: null,
    “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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37



    知识点:`@RestControllerAdvice`

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



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

    知识点:`@ExceptionHandler`

    说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常



    | 名称 | @ExceptionHandler |
    | :--: | :----------------------------------------------------------: |
    | 类型 | 方法注解 |
    | 位置 | 专用于异常处理的控制器方法上方 |
    | 作用 | 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行 |

    ### 项目异常处理方案

    异常处理器我们已经能够使用了,那么我们如何在项目中来处理异常呢?

    ### 异常分类

    因为异常的种类有很多,如果每一个异常都对应一个`@ExceptionHandler`,那得写多少个方法来处理各自的异常,所以我们在处理异常之前,需要对异常进行一个分类:

    - ```
    业务异常

    (BusinessException)

    • 规范的用户行为产生的异常
      • 用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串
    • 不规范的用户行为操作产生的异常
      • 如用户手改URL,故意传递错误数据localhost:8080/books/略略略
  • ```
    系统异常

    1
    2
    3
    4
    5
    6
    7
    8

    (SystemException)

    - 项目运行过程中可预计,但无法避免的异常
    - 如服务器宕机

    - ```
    其他异常

    (Exception)

    • 编程人员未预期到的异常
      • 如:系统找不到指定文件

将异常分类以后,针对不同类型的异常,要提供具体的解决方案

异常解决方案

  • ```
    业务异常

    1
    2
    3
    4
    5
    6
    7
    8

    (BusinessException)

    - 发送对应消息传递给用户,提醒规范操作
    - 大家常见的就是提示用户名已存在或密码格式不正确等

    - ```
    系统异常

    (SystemException)

    • 发送固定消息传递给用户,安抚用户
      • 系统繁忙,请稍后再试
      • 系统正在维护升级,请稍后再试
      • 系统出问题,请联系系统管理员等
    • 发送特定消息给运维人员,提醒维护
      • 可以发送短信、邮箱或者是公司内部通信软件
    • 记录日志
      • 发消息给运维和记录日志对用户来说是不可见的,属于后台程序
  • ```
    其他异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    (Exception)

    - 发送固定消息传递给用户,安抚用户
    - 发送特定消息给编程人员,提醒维护(纳入预期范围内)
    - 一般是程序没有考虑全,比如未做非空校验等
    - 记录日志

    ### 具体实现

    - 思路:

    1. 先通过自定义异常,完成BusinessException和SystemException的定义
    2. 将其他异常包装成自定义异常类型
    3. 在异常处理器类中对不同的异常进行处理

    - `步骤一:`自定义异常类

    - SystemException
    - BusinessException

    public class SystemException extends RuntimeException {

    private Integer code;
    
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    public SystemException() {
    }
    
    public SystemException(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;
    }
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16



    说明:

    - 让自定义异常类继承`RuntimeException`的好处是,后期在抛出这两个异常的时候,就不用在`try..catch..`或`throws`了
    - 自定义异常类中添加`code`属性的原因是为了更好的区分异常是来自哪个业务的

    - `步骤二:`将其他异常包成自定义异常
    假如在`BookServiceImpl`的`getById`方法抛异常了,该如何来包装呢?

    具体的包装方式有:

    - 方式一:`try{}catch(){}`在catch中重新throw我们自定义异常即可。
    - 方式二:直接`throw`自定义异常即可

    public Book getById(Integer id) {

    //模拟业务异常,包装成自定义异常
    if(id == 1){
        throw new BusinessException(Code.BUSINESS_ERR,"你别给我乱改URL噢");
    }
    //模拟系统异常,将可能出现的异常进行包装,转换成自定义异常
    try{
        int i = 1/0;
    }catch (Exception e){
        throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
    }
    return bookDao.getById(id);
    

    }

    1
    2
    3

    上面为了使`code`看着更专业些,我们在Code类中再新增需要的属性

    public class Code {

    public static final Integer SAVE_OK = 20011;
    public static final Integer UPDATE_OK = 20021;
    public static final Integer DELETE_OK = 20031;
    public static final Integer GET_OK = 20041;
    
    public static final Integer SAVE_ERR = 20010;
    public static final Integer UPDATE_ERR = 20020;
    public static final Integer DELETE_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 = 60001;
    

    }

    1
    2
    3

    - ```
    步骤三:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RestControllerAdvice
    public class ProjectExceptionAdvice {
    @ExceptionHandler(SystemException.class)
    public Result doSystemException(SystemException 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());
    }

    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex) {
    return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙,请稍后再试!");
    }
    }
  • ```
    步骤四:

    1
    2
    3
    4
    5

    运行程序

    根据ID查询,如果传入的参数为1,会报

    BusinessException

    1
    2
    3

    ,错误信息应为

    你别给我乱改URL噢

    1

    {

    "code": 60001,
    "data": null,
    "msg": "你别给我乱改URL噢"
    

    }

    1
    2
    3

    如果传入的是其他参数,会报

    SystemException

    1
    2
    3

    ,错误信息应为

    服务器访问超时,请重试!

    1

    {

    "code": 50002,
    "data": null,
    "msg": "服务器访问超时,请重试!"
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

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

    [![img](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)](https://pic.imgdb.cn/item/631d597316f2c2beb1d20ccd.jpg)

    ## 前后台协议联调

    ### 环境准备

    - 导入提供好的前端页面,如果想自己写页面,也可以用element-ui,有空了考虑考虑

    - 由于添加了静态资源,SpringMVC会拦截,所以需要在对静态资源放行

    - 新建SpringMVCSupport类,继承

    WebMvcConfigurationSupport

    1
    2
    3

    ,并重写

    addResourceHandlers()

    1
    2
    3

    方法

    @Configuration
    public class SpringMvcSupport extends WebMvcConfigurationSupport {

      @Override
      protected void addResourceHandlers(ResourceHandlerRegistry registry) {
          registry.addResourceHandler("/css/**").addResourceLocations("/css/");
          registry.addResourceHandler("/js/**").addResourceLocations("/js/");
          registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
          registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
      }
    

    }

    1
    2
    3

    - 同时也需要让SpringMvcConfig扫描到我们的配置类

    @Configuration
    @ComponentScan({“com.blog.controller”,”com.blog.config”})
    @EnableWebMvc
    public class SpringMvcConfig {
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    现在我们打开浏览器,访问`http://localhost:8080/pages/books.html`,应该是可以正常看到页面的

    ### 页面分析

    在完成增删改查操作之前,我们先来看看给我们提供的页面源码

    - 源码
    - 页头部分
    - 表格部分
    - 分页
    - 新增按钮弹出框
    - data
    - 钩子函数
    - methods
    - DIY
    - 最终页面

    <!DOCTYPE html>

SpringMVC案例

图书管理

查询新建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15



### 列表功能

需求:页面加载完后发送异步请求到后台获取列表数据进行展示

1. 找到页面的钩子函数,`created()`
2. `created()`方法中调用了`this.getAll()`方法
3. 在getAll()方法中使用axios发送异步请求从后台获取数据
4. 访问的路径为`http://localhost/books`
5. 返回数据

那么修改getAll()方法

res.data`表示获取的`Result`对象,而`Result`对象的`data`属性才是真正的数据 也就是将`Rusult.data`赋给了`this.dataList getAll() { axios.get("/books").then((res)=>{ this.dataList = res.data.data; }) }
1
2
3

在钩子函数中直接调用`getAll()`即可

created() { this.getAll(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

那么现在重启服务器,打开浏览器访问`http://localhost:8080/pages/books.html`,表格中可以正常显示数据了



### 添加功能

需求:完成图片的新增功能模块

1. 找到页面上的`新建`按钮,按钮上绑定了`@click="openSave()"`方法
2. 在method中找到`openSave`方法,方法中打开新增面板
3. 新增面板中找到`确定`按钮,按钮上绑定了`@click="saveBook()"`方法
4. 在method中找到`saveBook`方法
5. 在方法中发送请求和数据,响应成功后将新增面板关闭并重新查询数据

- ```
openSave
打开新增面板
1
2
3
openSave() {
this.dialogFormVisible = true;
}
- ``` saveBook
1
2
3

方法发送异步请求并携带数据

saveBook () { //发送ajax请求 axios.post("/books",this.formData).then((res)=>{ this.dialogFormVisible = false; this.getAll(); }); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

### 添加功能状态处理

基础的新增功能已经完成,但是还有一些问题需要解决下:

需求:新增成功是关闭面板,重新查询数据,那么新增失败以后该如何处理?

1. 在handlerAdd方法中根据后台返回的数据来进行不同的处理
2. 如果后台返回的是成功,则提示成功信息,并关闭面板
3. 如果后台返回的是失败,则提示错误信息



- 修改前端页面

saveBook() { axios.post("/books",this.formData).then((res)=>{ //20011是成功的状态码,成功之后就关闭对话框,并显示添加成功 if (res.data.code == 20011){ this.dialogFormVisible = false; this.$message.success("添加成功") //20010是失败的状态码,失败后给用户提示信息 }else if(res.data.code == 20010){ this.$message.error("添加失败"); //如果前两个都不满足,那就是SYSTEM_UNKNOW_ERR,未知异常了,显示未知异常的错误提示信息安抚用户情绪 }else { this.$message.error(res.data.msg); } }).finally(()=>{ this.getAll(); }) }
1
2
3
4

- 后台返回操作结果,将Dao层的增删改方法返回值从`void`改成`int`
如果添加失败,int值为0,添加成功则int值为显示受影响的行数

public interface BookDao { @Insert("insert into tbl_book values (null, #{type}, #{name}, #{description})") int save(Book book); @Update("update tbl_book set type=#{type}, `name`=#{name}, `description`=#{description} where id=#{id}") int update(Book book); @Delete("delete from tbl_book where id=#{id}") int delete(Integer id); @Select("select * from tbl_book where id=#{id}") Book getById(Integer id); @Select("select * from tbl_book") ListgetAll(); }
1
2
3
4

- 在BookServiceImpl中,增删改方法根据DAO的返回值来决定返回true/false
如果受影响的行大于0,则添加成功,否则添加失败

@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; public boolean save(Book book) { return bookDao.save(book) > 0; } public boolean update(Book book) { return bookDao.update(book) > 0; } public boolean delete(Integer id) { return bookDao.delete(id) > 0; } public Book getById(Integer id) { return bookDao.getById(id); } public ListgetAll() { return bookDao.getAll(); } }
1
2
3
4

处理完新增后,会发现新增还存在一个问题,
新增成功后,再次点击`新增`按钮会发现之前的数据还存在,这个时候就需要在新增的时候将表单内容清空。

// 重置表单 resetForm() { this.formData = {}; } // 弹出添加窗口 openSave() { this.dialogFormVisible = true; //每次弹出表单的时候,都重置一下数据 this.resetForm(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

### 修改功能

需求:完成图书信息的修改功能

1. 找到页面中的`编辑`按钮,该按钮绑定了`@click="openEdit(scope.row)"`
2. 在method的`openEdit`方法中发送异步请求根据ID查询图书信息
3. 根据后台返回的结果,判断是否查询成功
- 如果查询成功打开修改面板回显数据,如果失败提示错误信息
4. 修改完成后找到修改面板的`确定`按钮,该按钮绑定了`@click="handleEdit()"`
5. 在method的`handleEdit`方法中发送异步请求提交修改数据
6. 根据后台返回的结果,判断是否修改成功
- 如果成功提示错误信息,关闭修改面板,重新查询数据,如果失败提示错误信息

- scope.row代表的是当前行的行数据,也就是说,scope.row就是选中行对应的json数据,如下:

{ "id": 1, "type": "计算机理论", "name": "Spring实战 第五版", "description": "Spring入门经典教程,深入理解Spring原理技术内幕" }
1
2
3

- 修改openEdit()方法

openEdit(row) { axios.get("/books/" + row.id).then((res) => { if (res.data.code == 20041) { this.formData = res.data.data; this.dialogFormVisible4Edit = true; } else { this.$message.error(res.data.msg); } }); }
1
2
3

修改`handleUpdate`方法

handleEdit() { axios.put("/books", this.formData).then((res) => { if (res.data.code == 20021) { this.dialogFormVisible4Edit = false; this.$message.success("修改成功") } else if (res.data.code == 20020) { this.$message.error("修改失败") } else { this.$message.error(res.data.msg); } }).finally(() => { this.getAll(); }); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17



### 删除功能

需求:完成页面的删除功能。

1. 找到页面的删除按钮,按钮上绑定了`@click="delete(scope.row)"`
2. method的`delete`方法弹出提示框
3. 用户点击取消,提示操作已经被取消。
4. 用户点击确定,发送异步请求并携带需要删除数据的主键ID
5. 根据后台返回结果做不同的操作
- 如果返回成功,提示成功信息,并重新查询数据
- 如果返回失败,提示错误信息,并重新查询数据

- 修改

delete
1
2
3

方法

deleteBook(row) { this.$confirm("此操作永久删除当前数据,是否继续?","提示",{ type:'info' }).then(()=> { axios.delete("/books/" + row.id).then((res) => { if (res.data.code == 20031) { this.$message.success("删除成功") } else if (res.data.code == 20030) { this.$message.error("删除失败") } }).finally(() => { this.getAll(); }); }).catch(()=>{ this.$message.info("取消删除操作") }) }
1
2
3

至此增删改操作就都完成了,完整的前端代码如下

SpringMVC案例

图书管理

查询新建