SpringBoot笔记

仅为学习记录,方便回顾复习,如有侵权请联系删除!

SpringBoot

目录

文章目录

前言

什么是SpringBoot

介绍:SpringBoot由Pivotal团队开发,通过特定的配置方式,减少开发人员重复性的样板化配置工作。简单说就是SpringBoot整合了大部分框架。

1、简化spring应用开发的一个框架;

2、spring技术栈的一个大集合;

3、J2EE开发的一站式解决方案;

J2EE的全称是Java 2 Platform Enterprise Edition,它是由SUN公司领导、各厂家共同制定的工业标准,或者说,它是在SUN公司领导下,多家公司参与共同制定的企业级分布式应用程序开发规范。J2EE是市场上主流的企业级分布式应用平台的解决方案 。

J2EE百度百科:https://baike.baidu.com/item/j2ee/110838?fr=aladdin

使用SpringBoot的好处

如果不用SpringBoot,那么搭建一个Spring Web项目需要很多配置:web.xml、springMVC.xml、spring的配置文件、配置数据库环境、配置spring事务、开启注解、配置日志、部署tomcat等等。

如果用SpringBoot,可以通过很少的几个配置,就能快速搭建一套Web项目或构建一个微服务。

一、Spring Boot开发环境搭建和项目启动

1、jdk的配置

2、Spring Boot 工程的构建

2.1 IDEA快速构建

IDEA 中可以通过File->New->Project来快速构建 Spring Boot 工程。点击New Project后选择 Spring Initializr。设置好下面信息

Group:企业域名。这里使用com.chw;

Artifact:项目名称。这里使用springboot;

Dependencies:选择Spring Web即可;

后续不同课程将创建不同的module来编码。

2.2 官方构建

2.3 Maven配置

创建了 Spring Boot 项目之后,需要进行 maven 配置。打开File->settings,搜索 maven,在 Maven home directory 中选择本地 Maven 的安装路径;在 User settings file 中选择本地 Maven 的配置文件所在路径。

2.4 编码配置

ctrl+alt+s打开settings,搜索encoding,把Global Encoding、Project Encoding、Default encoding for properties files都修改为UTF-8。

3、Spring Boot项目工程结构

Spring Boot 项目总共有三个模块

  1. src/main/java路径:主要编写业务程序;
  2. src/main/resources路径:存放静态文件和配置文件;
  3. src/test/java路径:主要编写测试程序;

默认情况下,会创建一个启动类

Course01Application(路径:src/main/java/com/chw/course01/Course01Application.java)

package com.chw.course01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Course01Application {
    public static void main(String[] args) {
        SpringApplication.run(Course01Application.class, args);
    }
}

该类上面有个@SpringBootApplication注解,该启动类中有个 main 方法。Spring Boot 启动只要运行该 main 方法即可,非常方便。另外,Spring Boot 内部集成了 tomcat,不需要我们人为手动去配置 tomcat,开发者只需要关注具体的业务逻辑即可。

下面写一个demo测试Spring Boot能否成功启动

com/chw/course01/controller/StartController.java

package com.chw.course01.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author chw1113
 * @create 2022-07-14 16:16
 */
@RestController
@RequestMapping("/start")
public class StartController {
    @RequestMapping("/springboot")
    public String startSpringBoot() {
        return "Welcome to Spring Boot";
    }
}

启动com/chw/course01/Course01Application.java中的main方法

在浏览器中输入 localhost:8080/start/springboot访问,页面显示Welcome to Spring Boot即项目启动成功。

端口号默认为8080,如果想修改,可在 application.yml 文件中使用 server.port 来人为指定端口,

如8001端口:

server:
	port:8001

4、总结

以上学习了快速创建和启动Spring Boot工程。

二、Spring Boot返回Json数据及数据封装

在项目开发中,接口与接口之间,前后端之间数据的传输都使用 Json 格式,在 Spring Boot 中,接口返回 Json 格式的数据很简单,只需在 Controller 中使用@RestController注解标注在类上,即可使该类的所有方法的返回值都转换为 Json 格式的数据。

Spring Boot 中默认使用的 json 解析框架是 jackson。下面我们看一下默认的 jackson 框架对常用数据类型的转 Json 处理。

1、Spring Boot默认对Json的处理

在实际项目中,常用的数据结构无非有类对象、List对象、Map对象,我们看一下默认的 jackson 框架对这三个常用的数据结构转成 json 后的格式如何。

1.1 创建User实体类

com/chw/course02/pojo/User.java

package com.chw.course02.pojo;

public class User {
    private Long id;
    private String username;
    private String password;
 	//省略get set方法、构造器、重写toString()
}

1.2 创建controller类

com/chw/course02/controller/JsonController.java

package com.chw.course02.controller;
import com.chw.course02.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/json")
public class JsonController {

    @RequestMapping("/user")
    public User getUser() {
        return new User(1L, "Ganon", "123123");
    }

    @RequestMapping("/list")
    public List<User> getUserList() {
        ArrayList<User> userList = new ArrayList<>();
        User user1 = new User(1L, "zelda", "123123");
        User user2 = new User(2L, "link", "123321");
        userList.add(user1);
        userList.add(user2);
        return userList;
    }

    @RequestMapping("/map")
    public Map<String, Object> getMap() {
        Map<String, Object> map = new HashMap<>();
        User user = new User(1L, "zelda", "123123");
        map.put("用户信息", user);
        map.put("职业", "公主");
        map.put("住址", "海拉鲁城堡");
        map.put("身高cm", 165);
        return map;
    }
}

1.3 测试不同数据类型返回的json

启动main方法测试(com/chw/course02/Course02Application.java)。

浏览器访问:localhost:8080/json/user,得到

{"id":1,"username":"Ganon","password":"123123"}

浏览器访问:localhost:8080/json/list,得到

[{"id":1,"username":"zelda","password":"123123"},{"id":2,"username":"link","password":"123321"}]

浏览器访问:localhost:8080/json/map,得到

{"用户信息":{"id":1,"username":"zelda","password":"123123"},"职业":"公主","住址":"海拉鲁城堡","身高cm":165}

可以看出,map中不管是什么数据类型,都可以转成响应的json格式,非常方便。

1.4 json中对null的处理

实际项目中,我们希望null值在转json格式之后输出为""这种空字符串,则在Spring Boot中需要进行配置。

新建一个jackson的配置类:

com/chw/course02/config/JacksonConfig.java

package com.chw.course02.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.io.IOException;

@Configuration
public class JacksonConfig {
    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            @Override
            public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString("");
            }
        });
        return objectMapper;
    }
}

改一下JsonController的getMap()方法,将几个值改为null测试一下

com/chw/course02/controller/JsonController.java

@RequestMapping("/map")
public Map<String, Object> getMap() {
    Map<String, Object> map = new HashMap<>();
    User user = new User(1L, "zelda", "123123");
    map.put("用户信息", user);
    map.put("职业", null);
    map.put("住址", "海拉鲁城堡");
    map.put("身高", 165);
    return map;
}

浏览器访问localhost:8080/json/map,结果:

未写jackson配置类之前

{"用户信息":{"id":1,"username":"zelda","password":"123123"},"职业":null,"住址":"海拉鲁城堡","身高":165}

写了jackson配置类之后

{"用户信息":{"id":1,"username":"zelda","password":"123123"},"职业":"","住址":"海拉鲁城堡","身高":165}

2、使用阿里巴巴FastJson的设置

2.1 jackson和fastJson的对比

选项fastJsonjackson
上手难易容易中等
高级特性支持中等丰富
官方文档、Example支持中文英文
处理json速度略快

2.2 fastJson依赖导入

使用fastJson需要导入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.71</version>
</dependency>

2.3 使用fastJson处理null

fastJson对null的处理和jackson不同,需要继承WebMvcConfigurationSupport类,覆盖configureMessageConverters方法,方法中可以选择null转换的场景。

(先把JacksonConfig类的注解都注释掉,fastJson的配置类才能顺利生效)

com/chw/course02/config/fastJsonConfig.java

package com.chw.course02.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class fastJsonConfig extends WebMvcConfigurationSupport {

    /**
     * 使用案例 FastJson 作为JSON MessageConverter
     * @param converters
     */
    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        config.setSerializerFeatures(
                // 保留map空的字段
                SerializerFeature.WriteMapNullValue,
                // 将String类型的null转成""
                SerializerFeature.WriteNullStringAsEmpty,
                // 将Number类型的null转成0
                SerializerFeature.WriteNullNumberAsZero,
                // 将List类型的null转成[]
                SerializerFeature.WriteNullListAsEmpty,
                // 将Boolean类型的null转成false
                SerializerFeature.WriteNullBooleanAsFalse,
                // 避免循环引用
                SerializerFeature.DisableCircularReferenceDetect
        );
        converter.setFastJsonConfig(config);
        converter.setDefaultCharset(Charset.forName("UTF-8"));
        ArrayList<MediaType> mediaTypeList = new ArrayList<>();
        // 解决中文乱码问题,相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        converter.setSupportedMediaTypes(mediaTypeList);
        converters.add(converter);
    }
}

注意map的空字段只是保留为null,并没有转换为空字符串""

// 保留map空的字段
SerializerFeature.WriteMapNullValue

3、封装统一返回的数据结构

在实际项目中,除了要封装数据之外,往往需要在返回的 json 中添加一些其他信息,比如状态码 code 或一些 msg 给调用者,使得调用者可以根据 code 或者 msg 做一些逻辑判断。

所以在实际项目中,我们需要封装一个统一的 json 结构来存储返回的信息。

3.1 定义统一的json结构

由于封装的 json 数据的类型不确定,所以在定义统一的 json 结构时,需要用到泛型。

统一的 json 结构中的属性包括数据、状态码、提示信息即可,构造方法可以根据实际业务需求做添加,一般来说,需要有默认的返回结构,也需要有用户指定的返回结构。如下:

com/chw/course02/config/JsonResult.java

package com.chw.course02.config;

public class JsonResult<T> {
    private T data;
    private String code;
    private String msg;
    
    /**
     * 若没有数据返回,默认状态码为0,提示信息为:操作成功!
     */
    public JsonResult() {
        this.code = "0";
        this.msg = "操作成功!";
    }

    /**
     * 若没有数据返回,自定义状态码和提示信息
     */
    public JsonResult(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 有数据返回时,状态码为0,默认提示信息为:操作成功!
     *
     * @param data
     */
    public JsonResult(T data) {
        this.data = data;
        this.code = "0";
        this.msg = "操作成功!";
    }

    /**
     * 有数据返回,状态码为0,自定义提示信息
     *
     * @param data
     * @param msg
     */
    public JsonResult(T data, String msg) {
        this.data = data;
        this.code = "0";
        this.msg = msg;
    }
	/*省略get set方法*/
}

3.2 修改controller中的返回值类型及测试

由于 JsonResult 使用了泛型,所以所有的返回值类型都可以使用该统一结构,在具体的场景将泛型替换成具体的数据类型即可,非常方便,也便于维护。在实际项目中,还可以继续封装,比如状态码和提示信息可以定义一个枚举类型,以后我们只需要维护这个枚举类型中的数据即可(在本课程中就不展开了)。

根据上方的JsonResult来重新写一个Controller

com/chw/course02/controller/JsonResultController.java

package com.chw.course02.controller;
import com.chw.course02.config.JsonResult;
import com.chw.course02.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/jsonresult")
public class JsonResultController {
    
    @RequestMapping("/user")
    public JsonResult<User> getUser() {
        User user = new User(1L, "波克布林", "123123");
        return new JsonResult<>(user);
    }
    
    @RequestMapping("/list")
    public JsonResult<List> getUserList() {
        List<User> userList = new ArrayList<>();
        User user1 = new User(1L, "莫力布林", "123123");
        User user2 = new User(2L, "波克布林", "321321");
        userList.add(user1);
        userList.add(user2);
        return new JsonResult<>(userList, "获取用户列表成功");
    }

    @RequestMapping("/map")
    public JsonResult<Map> getMap() {
        Map<String, Object> map = new HashMap<>(3);
        User user = new User(1L, "波克布林", null);
        map.put("角色信息", user);
        map.put("住址", "海拉鲁大陆");
        map.put("技能", null);
        map.put("身高cm", 140);
        return new JsonResult<>(map);
    }
}

测试:启动main方法(com/chw/course02/Course02Application.java)

浏览器访问localhost:8080/jsonresult/user,结果

{"code":"0","data":{"id":1,"password":"123123","username":"波克布林"},"msg":"操作成功!"}

浏览器访问localhost:8080/jsonresult/list,结果

{"code":"0","data":[{"id":1,"password":"123123","username":"莫力布林"},{"id":2,"password":"321321","username":"波克布林"}],"msg":"获取用户列表成功"}

浏览器访问localhost:8080/jsonresult/map,结果

{
    "code":"0",
    "data":{ "住址":"海拉鲁大陆",
             "身高cm":140,
             "技能":null,
             "角色信息":{"id":1,"password":"","username":"波克布林"}
    },
    "msg":"操作成功!"
}

这种封装不但将数据通过 json 传给前端或者其他接口,还附带状态码和提示信息,这种做法在实际项目场景中应用广泛。

4、总结

本节对 Spring Boot 中 json 数据的返回做了详细的分析,讲解了 Spring Boot 默认的 jackson 框架和阿里巴巴的 fastJson 框架及其配置。

另外,结合实际项目,总结了实际项目中使用的 json 封装结构体,加入了状态码和提示信息,使得返回的 json 数据信息更加完整。

三、Spring Boot使用slf4j进行日志记录

IDEA模块切换至course03-slf4j

image-20220715081547211

开发中经常使用 System.out.println() 来打印一些信息,但是大量使用 System.out 会增加资源的消耗。

在实际项目中使用 slf4j 的 logback 来输出日志,效率高。

Spring Boot 提供了一套日志系统,logback 是最优的选择。

1、slf4j介绍

百度百科词条:

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。

Facade 美[fəˈsɑːd]:n.外观; (建筑物的)正面,立面; (虚假的)表面,外表;

在项目中使用了 slf4j 记录日志,只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。

使用 slf4j 记录日志非常简单,直接使用 LoggerFactory 创建即可。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test{
	private static final Logger logger = LoggerFactory.getLogger(Test.class);
    //....
}

2、application.yml中对日志的配置

Spring Boot 对 slf4j 支持的很好,内部已经集成了 slf4j,一般我们在使用的时候,会对slf4j 做一下配置。application.yml 文件是 Spring Boot 中唯一一个需要配置的文件。

一开始创建工程的时候是 application.properties 文件,但是推荐使用yml文件,因为 yml 文件的层次感特别好,更直观。但 yml 文件对格式要求较高,比如英文冒号后面必须有个空格,否则项目可能无法启动,而且也不报错。

用 properties 还是 yml 视个人习惯而定,都可以。这里我们使用 yml。新建application.yml文件后,要删除application.properties文件

看一下 application.yml 文件中对日志的配置:

logging:
  config: D:\workspace\workspace_idea1\springboot\course03-slf4j\src\main\resources\logback.xml
  level:
    com.chw.bootdemo.mbg.mapper: trace
    #开发时设置成 trace 方便定位问题。在生产环境上,将这个日志级别再设置成 error 级别即可。

logging.config 用来指定项目启动时,读取哪个配置文件。这里指定的是日志配置文件是根路径下的 logback.xml 。关于日志的配置,都放在 logback.xml 文件中。

logging.level 用来指定具体的 mapper 中日志的输出级别,上面的配置表示 com.chw.course03.dao 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题。在生产环境上,将这个日志级别再设置成 error 级别即可。

常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG。

3、logback.xml 配置文件解析及依赖

上面 application.yml 文件中指定了日志配置文件 logback.xml,这个文件主要用来做日志的相关配置。

在 logback.xml 中可以定义日志输出的格式、路径、控制台输出格式、文件大小、保存时长等等。

logback的maven依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

3.1定义日志输出格式和存储路径

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="FILE_PATH" value="D:/workspace/logs/course03/demo.%d{yyyy-MM-dd}.%i.log"/>
</configuration>

含义:首先定义一个格式,命名为 “LOG_PATTERN”,该格式中 %date 表示日期,%thread 表示线程名,%-5level 表示级别从左显示5个字符宽度,%logger{36} 表示 logger 名字最长36个字符,%msg 表示日志消息,%n 是换行符。

再定义名为 “FILE_PATH” 文件路径,日志都会存储在该路径下。%i 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引,日志文件允许的大小可以设置,下面会讲解。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须要是绝对路径。

3.2 定义控制台输出

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--按照上面的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
</configuration>

使用<appender>节点设置个控制台输出(class=“ch.qos.logback.core.ConsoleAppender”)的配置,定义为 “CONSOLE”。使用上面定义好的输出格式(LOG_PATTERN)来输出,使用 ${} 引用进来即可。

3.3 定义日志文件的相关参数

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按照上面配置的FILE_PATH路径来保存日志-->
            <fileNamePattern>${FILE_PATH}</fileNamePattern>
            <!--日志保存15天-->
            <maxHistory>15</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--单个日志文件的最大文件大小,超过则新建日志文件存储-->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>

        <encoder>
            <!--按照上面配置的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
</configuration>

使用<appender>定义一个名为 “FILE” 的文件配置,主要是配置日志文件保存的时间、单个日志文件存储的大小、以及文件保存的路径和日志的输出格式。

3.4 定义日志输出级别

src/main/resources/logback.xml

<configuration>
    <logger name="com.chw.course03" level="INFO"/>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

有了之前那些定义后,最后定义一下项目中默认的日志输出级别,这里定义级别为 INFO,然后针对 INFO 级别的日志,引用上面定义好的控制台日志输出和日志文件的参数。这样 logback.xml 文件中的配置就设置完了。

整体配置信息如下

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="FILE_PATH" value="D:/workspace/logs/course03/demo.%d{yyyy-MM-dd}.%i.log"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--按照上面的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按照上面配置的FILE_PATH路径来保存日志-->
            <fileNamePattern>${FILE_PATH}</fileNamePattern>
            <!--日志保存15天-->
            <maxHistory>15</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--单个日志文件的最大文件大小,超过则新建日志文件存储-->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>

        <encoder>
            <!--按照上面配置的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <logger name="com.chw.course03" level="INFO"/>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

4、使用Logger在项目中打印日志

在代码中,一般使用 Logger 对象来打印出一些 log 信息,可以指定打印出的日志级别,也支持占位符,很方便。

com/chw/course03/controller/TestController.java

package com.chw.course03.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    private final static Logger logger = LoggerFactory.getLogger(TestController.class);

    @RequestMapping("/log")
    public String testLog() {
        logger.debug("====测试日志debug级别打印====");
        logger.info("====测试日志info级别打印====");
        logger.error("====测试日志error级别打印====");
        logger.warn("====测试日志warn级别打印====");

        //使用占位符打印出一些参数信息
        String str1 = "the breath of wild";
        String str2 = "the legend of zelda";
        logger.info("===openworld's top : {}; version: {}", str2, str1);
        return "success";
    }
}

测试:启动main方法(com/chw/course03/Course03Slf4jApplication.java)

浏览器访问:localhost:8080/test/log,结果页面显示success

返回查看控制台输出内容

07:55:10.030 [http-nio-8080-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
07:55:10.030 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
07:55:10.036 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 6 ms
07:55:10.059 [http-nio-8080-exec-1] INFO  c.c.c.controller.TestController - ====测试日志info级别打印====
07:55:10.059 [http-nio-8080-exec-1] ERROR c.c.c.controller.TestController - ====测试日志error级别打印====
07:55:10.059 [http-nio-8080-exec-1] WARN  c.c.c.controller.TestController - ====测试日志warn级别打印====
07:55:10.059 [http-nio-8080-exec-1] INFO  c.c.c.controller.TestController - ===openworld's top : the legend of zelda; version: the breath of wild

因为 INFO 级别比 DEBUG 级别高,所以 debug 这条没有打印出来,如果将 logback.xml 中的日志级别设置成 DEBUG,那么四条语句都会打印出来。

打开 D:/workspace/logs/course03/ 目录可以发现生成的日志文件。

在项目部署后,大部分时间都是通过查看日志文件来定位问题。

image-20220715075759136

5、总结

简单介绍了slf4j,以及如何在Spring Boot中使用slf4j输出日志,分析logback.xml文件如何配置等等。

四、Spring Boot中的项目属性配置

在项目中,很多时候需要用到一些配置的信息,这些信息可能在测试环境和生产环境下会有不同的配置,后面根据实际业务情况有可能还会做修改,针对这种情况,我们不能将这些配置在代码中写死,最好就是写到配置文件中。

IDEA模块切换至course04-yml

image-20220715091832207

先配置一下日志

src/main/resources/application.yml

logging:
  config: D:\workspace\workspace_idea1\springboot\course04-yml\src\main\resources\logback.xml
  level:
    com.chw.course03.dao: trace

src/main/resources/logback.xml怎么配置请参考 第03课

1、少量配置信息的情形

举例,在微服务架构中,最常见的就是服务1需要调用服务2来获取服务2的相关信息,那么在服务1的配置文件中需要配置服务2的地址。

比如在当前服务1里,我们需要调用订单微服务2获取订单相关的信息,假设订单服务2的端口号是 8002,那我们可以做如下配置:

src/main/resources/application.yml

server:
  port: 8001
  
#配置微服务的地址
url:
  #订单微服务的地址
  orderUrl: http://localhost:8002

然后在业务代码中如何获取到这个配置的订单服务地址呢?可以使用 @Value 注解来解决。

在对应的类中加上一个属性,在属性上使用 @Value 注解即可获取到配置文件中的配置信息,如下:

com/chw/course04/controller/ConfigController.java

package com.chw.course04.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class ConfigController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigController.class);
    
    @Value("${url.orderUrl}")
    private String orderUrl;

    @RequestMapping("/config")
    public String testConfig() {
        LOGGER.info("====获取的订单服务地址为:{}", orderUrl);
        return "success";
    }
}

@Value 注解上通过 ${key} 即可获取配置文件中和 key 对应的 value 值。

测试:启动main方法(com/chw/course04/Course04YmlApplication.java)

浏览器访问:localhost:8001/test/config,页面显示success

控制台输出内容

08:31:30.436 [http-nio-8001-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
08:31:30.436 [http-nio-8001-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
08:31:30.442 [http-nio-8001-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 6 ms
08:31:30.468 [http-nio-8001-exec-1] INFO  c.c.c.controller.ConfigController - ====获取的订单服务地址为:http://localhost:8002

说明成功获取到了配置文件中的订单微服务地址,在实际项目中也是这么用的。后面如果因为服务器部署的原因,需要修改某个服务的地址,那么只要在配置文件中修改即可。

2、多个配置信息的情形

随着业务复杂度的增加,一个项目中可能会有越来越多的微服务,某个模块可能需要调用多个微服务获取不同的信息,那么就需要在配置文件中配置多个微服务的地址。可是,在需要调用这些微服务的代码中,如果这样一个个去使用 @Value 注解引入相应的微服务地址的话,太过于繁琐,也不科学。

所以,在实际项目中,业务繁琐,逻辑复杂的情况下,需要考虑封装一个或多个配置类。

举例,假如某个业务需要同时调用订单微服务、用户微服务和购物车微服务,分别获取订单、用户和购物车相关信息,然后对这些信息做一定的逻辑处理。那么在配置文件中,我们需要将这些微服务的地址都配置好:

src/main/resources/application.yml

server:
  port: 8001

#配置微服务的地址
url:
  # 订单微服务的地址
  orderUrl: http://localhost:8002
  # 用户微服务的地址
  userUrl: http://localhost:8003
  # 购物车微服务的地址
  shoppingUrl: http://localhost:8004

logging:
  config: D:\workspace\workspace_idea1\springboot\course04-yml\src\main\resources\logback.xml
  level:
    com.chw.course03.dao: trace

可能实际中远远不止三个微服务,而是十几个或者几十个。对于这种情况,可以先定义一个MicroServiceUrl类来专门保存微服务的url,如下

com/chw/course04/util/MicroServiceUrl.java

package com.chw.course04.util;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "url")
public class MicroServiceUrl {
    private String orderUrl;
    private String userUrl;
    private String shoppingUrl;
    //此处省略 get、set方法
}

使用 @ConfigurationProperties 注解并且使用 prefix 来指定一个前缀,然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可。属性的值就是url的http地址(如:http://localhost:8002)

即:前缀名 + 属性名就是配置文件中定义的 key。同时,该类上面需要加上 @Component 注解,把该类作为组件放到Spring容器中,让 Spring 去管理,我们使用的时候直接注入即可。

注意@ConfigurationProperties注解需要导入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

下面写个Controller测试一下。此时,不需要在代码中一个个引入这些微服务的 url 了,直接通过 @Resource 注解将刚刚写好配置类注入进来即可使用,非常方便。

com/chw/course04/controller/TestController.java

package com.chw.course04.controller;
import com.chw.course04.util.MicroServiceUrl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@RequestMapping("/testmicro")
public class TestController {

    private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);

    @Resource
    private MicroServiceUrl microServiceUrl;

    @RequestMapping("/micro")
    public String testConfig() {
        LOGGER.info("====获取的订单服务地址为:{}", microServiceUrl.getOrderUrl());
        LOGGER.info("====获取的订单服务地址为:{}", microServiceUrl.getUserUrl());
        LOGGER.info("====获取的订单服务地址为:{}", microServiceUrl.getShoppingUrl());
        return "success";
    }
}

测试:启动main方法(com/chw/course04/Course04YmlApplication.java)

浏览器访问:localhost:8001/testmicro/micro,页面显示success

控制台输出内容

09:00:09.016 [http-nio-8001-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
09:00:09.017 [http-nio-8001-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
09:00:09.022 [http-nio-8001-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 5 ms
09:00:09.041 [http-nio-8001-exec-1] INFO  c.c.c.controller.TestController - ====获取的订单服务地址为:http://localhost:8002
09:00:09.042 [http-nio-8001-exec-1] INFO  c.c.c.controller.TestController - ====获取的订单服务地址为:http://localhost:8003
09:00:09.042 [http-nio-8001-exec-1] INFO  c.c.c.controller.TestController - ====获取的订单服务地址为:http://localhost:8004

3、指定项目配置文件

我们知道,在实际项目中,一般有两个环境:开发环境和生产环境。开发环境中的配置和生产环境中的配置往往不同,比如:环境、端口、数据库、相关地址等等。我们不可能在开发环境调试好之后,部署到生产环境后,又要将配置信息全部修改成生产环境上的配置,这样太麻烦,也不科学。

最好的解决方法就是开发环境和生产环境都有一套对用的配置信息,然后当我们在开发时,指定读取开发环境的配置,当我们将项目部署到服务器上之后,再指定去读取生产环境的配置。

我们新建两个配置文件: application-dev.ymlapplication-pro.yml,分别用来对开发环境和生产环境进行相关配置。这里为了方便,我们分别设置两个访问端口号,开发环境用 8001,生产环境用 8002.

src/main/resources/application-dev.yml

#开发环境配置文件
server:
  port: 8001

#配置微服务的地址
url:
  # 订单微服务的地址
  orderUrl: http://localhost:8002
  # 用户微服务的地址
  userUrl: http://localhost:8003
  # 购物车微服务的地址
  shoppingUrl: http://localhost:8004

logging:
  config: D:\workspace\workspace_idea1\springboot\course04-yml\src\main\resources\logback.xml
  level:
    com.chw.course03.dao: trace

src/main/resources/application-pro.yml

#生产环境配置文件
server:
  port: 8002
  
#生产环境的配置.........

然后在 application.yml 文件中指定读取哪个配置文件即可。

比如我们在开发环境下,指定读取 applicationn-dev.yml 文件,如下:

src/main/resources/application.yml

spring:
  profiles:
    active:
      - dev

这样就可以在开发的时候,指定读取 application-dev.yml 文件,访问的时候使用 8001 端口,部署到服务器后,只需要将 application.yml 中指定的文件改成 application-pro.yml 即可,然后使用 8002 端口访问,非常方便。

4、总结

本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。

五、Spring Boot中的MVC支持

Spring Boot 的 MVC 支持主要来介绍实际项目中最常用的几个注解,

包括 @RestController@RequestMapping@PathVariable@RequestParam 以及 @RequestBody

主要介绍这几个注解常用的使用方式和特点。

1、@RestController

@RestController 是 Spring Boot 新增的一个注解,我们看一下该注解都包含了哪些东西。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    String value() default "";
}

@RestController 注解包含了原来的 @Controller 和 @ResponseBody 注解,@ResponseBody 注解是将返回的数据结构转换为 Json 格式。

所以 @RestController 可以看作是 @Controller 和 @ResponseBody 的结合体,使用 @RestController 之后就不用再使用 @Controller 了。

注意一个问题:

如果是前后端分离,不适用模板渲染的话,比如 Thymeleaf,这种情况下是可以直接使用@RestController 将数据以 json 格式传给前端,前端拿到之后解析;

但如果不是前后端分离,需要使用模板来渲染的话,一般 Controller 中都会返回到具体的页面,那么此时就不能使用@RestController了,比如:

public String getUser(){
    return "user";
}

这是如果是要返回到user.html页面的话,如果使用@RestController的话,会将user作为字符串返回,所以不能用@RestController,要用@Controller。下一节Spring Boot集成Thymeleaf模板引擎中会再说明。

2、@RequestMapping

@RequestMapping 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。

该注解有6个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。

  • value 属性:指定请求的实际地址,value 可以省略不写
  • method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET
  • produces属性:指定返回内容类型,如 produces = “application/json; charset=UTF-8”

针对四种不同的请求方式,是有相应注解的,不用每次在 @RequestMapping 注解中加 method 属性来指定

  • @GetMapping
  • @PostMapping
  • @DeleteMapping
  • @PutMapping

3、@PathVariable

@PathVariable 注解主要是用来获取 url 参数,Spring Boot 支持 restful 风格的 url,比如一个 GET 请求携带一个参数 id 过来,我们将 id 作为参数接收,可以使用 @PathVariable 注解。如下:

@GetMapping("/user/{id}")
public String testPathVariable(@PathVariable Interger id){
    System.out.println("获取到的id为:"+id);
    return "success";
}

注意,如果想要url中占位符中的id值直接赋值到参数id中,需要保证url中的参数和方法接收参数一致,否侧无法接收。当不一致时,需要用@PathVariable中的value属性来约定对应关系,如

@RequestMapping("/user/{userid}")
public String testPathVariable(@PathVariable(value = "userid") Integer id){
    System.out.println("获取到的id为:" + id);
    return "success";
}

对于访问的url,占位符的位置可以在任何位置,不一定非要在最后,比如:/xxx/{id}/user

另外,url支持多个占位符,方法参数使用同样数量的参数来接收,如

@GetMapping("/user/{userid}/{name}")
public String testPathVariable(@PathVariable(value = "userid") Integer id, @PathVariable String name){
    System.out.println("获取到的id为:" + id);
    System.out.println("获取到的name为:" + name);
    return "success";
}

同样地,如果 url 中的参数和方法中的参数名称不同的话,也需要使用 value 属性来约定。

4、@RequestParam

@RequestParam 注解顾名思义,也是获取请求参数的。

@RequestParam 和 @PathVariable 的不同之处在于:

  • @PathValiable 是从 url 模板中获取参数值, 即这种风格的 url:http://localhost:8080/user/{id} ;

  • @RequestParam 是从 request 里面获取参数值,即这种风格的 url:http://localhost:8080/user?id=1

假设当前url为http://localhost:8080/user?id=1

controller方法为

@GetMapping("/user")
public String testRequestParam(@RequestParam Integer id){
    System.out.println("获取到的id为:" + id);
    return "success";
}

可以正常从控制台打印出 id 信息。

同样地,url 上面的参数和方法的参数需要一致,不一致时,需使用 value 属性来说明。

如 url 为:http://localhost:8080/user?userid=1

@RequestMapping("/user")
public String testRequestParam(@RequestParam(value = "userid", required = false) Integer id){
    System.out.println("获取到的id为:" + id);
    return "success";
}

除了 value 属性外,还有个两个属性比较常用:

required 属性:true 表示该参数必须要传,否则就会报 404 错误,false 表示可有可无。
defaultValue 属性:默认值,表示如果请求中没有同名参数时的默认值。

@RequestParam 注解用于 GET 请求上时,接收拼接在 url 中的参数。

此外,@RequestParam 注解还可以用于 POST 请求,接收前端表单提交的参数,假如前端通过表单提交 username 和 password 两个参数,那我们可以使用 @RequestParam 来接收,用法和上面一样。

@PostMapping("/form1")
public String testForm(@RequestParam String username, @RequestParam String password) {
    System.out.println("获取到的username为:" + username);
    System.out.println("获取到的password为:" + password);
    return "success";
}

如果表单数据很多,则需要封装一个实体类来接收这些参数,实体中的属性名和表单中的参数名一致即可。

实体类:

public class User {
	private String username;
	private String password;
	// set get 方法
}

controller方法:

@PostMapping("/form2")
public String testForm(User user) {
    System.out.println("获取到的username为:" + user.getUsername());
    System.out.println("获取到的password为:" + user.getPassword());
    return "success";
}

5、@RequestBody

如果前端传过来的不是表单提交的信息,而是json格式的实体,此时就要用@RequestBody接收。

@RequestBody 注解用于接收前端传来的实体,接收参数也是对应的实体,比如前端通过 json 提交传来两个参数 username 和 password,此时我们需要在后端封装一个实体来接收。在传递的参数比较多的情况下,使用 @RequestBody 接收会非常方便。例如:

后端封装的实体类

public class User {
	private String username;
	private String password;
	// set get 方法
}

controller方法

@PostMapping("/user")
public String testRequestBody(@RequestBody User user) {
	System.out.println("获取到的username为:" + user.getUsername());
	System.out.println("获取到的password为:" + user.getPassword());
	return "success";
}

@RequestBody 注解用于 POST 请求上,接收 json 实体参数。它和上面我们介绍的表单提交有点类似,只不过参数的格式不同,一个是 json 实体,一个是表单提交。在实际项目中根据具体场景和需要使用对应的注解即可。

注意:@ResponseBody如果标注在方法上,那么方法返回的数据会转换成json格式。如果标注在类上,那么类的所有方法返回的数据都会转换成json格式。

如果@ResponseBody标注在方法形参前面,那么将用来接收前端传过来的json数据,赋值给这个形参。

6、总结

本节课主要讲解了 Spring Boot 中对 MVC 的支持,分析了 @RestController、 @RequestMapping、@PathVariable、 @RequestParam 和 @RequestBody 四个注解的使用方式。

以上四个注解是使用频率很高的注解,在所有的实际项目中基本都会遇到,要熟练掌握。

六、Spring Boot 集成 Swagger2 展现在线接口文档

IDEA模块切换至course06-swagger2

image-20220715160755214

1、Swagger简介

1.1 解决的问题

随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了前后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。前端和后端的唯一联系,变成了 API 接口,所以 API 文档变成了前后端开发人员联系的纽带,变得越来越重要。

那么问题来了,随着代码的不断更新,开发人员在开发新的接口或者更新旧的接口后,由于开发任务的繁重,往往文档很难持续跟着更新,Swagger 就是用来解决该问题的一款重要的工具,对使用接口的人来说,开发人员不需要给他们提供文档,只要告诉他们一个 Swagger 地址,即可展示在线的 API 接口文档,除此之外,调用接口的人员还可以在线测试接口数据,同样地,开发人员在开发接口时,同样也可以利用 Swagger 在线接口文档测试接口数据,这给开发人员提供了便利。

1.2 Swagger官方

Swagger官网:https://swagger.io/

本文主要讲解在 Spring Boot 中如何导入 Swagger2 工具来展现项目中的接口文档。

2、Swagger2的maven依赖

使用 Swagger2 工具,必须要导入 maven 依赖

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.2.2</version>
</dependency>

3、Swagger2的配置

使用 Swagger2 需要进行配置,Spring Boot 中对 Swagger2 的配置非常方便,新建一个配置类,Swagger2 的配置类上除了添加必要的 @Configuration 注解外,还需要添加 @EnableSwagger2 注解。

com/chw/course06/config/SwaggerConfig.java

package com.chw.course06.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 指定构建api文档的详细信息的方法:apiInfo()
                .apiInfo(apiInfo())
                .select()
                // 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口
                .apis(RequestHandlerSelectors.basePackage("com.chw.course06.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 设置页面标题
                .title("Spring Boot集成Swagger2接口总览")
                // 设置接口描述
                .description("欢迎来到荒野之息的流氓课")
                // 设置联系方式
                .contact(new Contact("Link", "HyRuleCastle", "link@hero.com"))
                // 设置版本
                .version("1.0")
                // 构建
                .build();
    }
}

到此为止,我们已经配置好了 Swagger2 了。现在我们可以测试一下配置有没有生效,启动项目,在浏览器中输入 localhost:8080/swagger-ui.html,即可看到 swagger2 的接口页面,如下图所示,说明Swagger2 集成成功。

注意有时候访问失败是Swagger2的版本和Spring Boot版本不兼容的问题。

4、Swagger2的使用

上面我们已经配置好了 Swagger2,并且也启动测试了一下,功能正常,下面我们开始使用 Swagger2,主要来介绍 Swagger2 中的几个常用的注解,分别在实体类上、 Controller 类上以及 Controller 中的方法上,最后我们看一下 Swagger2 是如何在页面上呈现在线接口文档的,并且结合 Controller 中的方法在接口中测试一下数据。

4.1 实体类注解

com/chw/course06/pojo/User.java

package com.chw.course06.pojo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(value = "用户实体类")
public class User {
    
    @ApiModelProperty(value = "用户唯一标识")
    private Long id;
    
    @ApiModelProperty(value = "用户姓名")
    private String username;

    @ApiModelProperty(value = "用户密码")
    private String password;
	//get set 方法,构造器
}

注解解释

@ApiModel 用于实体类,表示对类进行说明,用于参数用实体类接收。

@ApiModelProperty 用于类中属性,表示对model属性的说明或者数据操作更改。

下文将介绍在线api文档中的效果

4.2 Controller 类中相关注解

写一个 TestController,再写几个接口,然后学习一下 Controller 中和 Swagger2 相关的注解。

com/chw/course06/controller/TestController.java

(先从course02模块中把JsonResult.java复制过来放在com/chw/course06/pojo下)

package com.chw.course06.controller;
import com.chw.course06.pojo.JsonResult;
import com.chw.course06.pojo.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/swagger")
@Api(value = "Swagger2 在线接口文档")
public class TestController {

    @GetMapping("/get/{id}")
    @ApiOperation(value = "根据用户唯一标识获取用户信息")
    public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id){
        // 模拟数据库中根据id获取User信息
        User user = new User(id, "Zelda", "123123");
        return new JsonResult(user);
    }
}

注解解释

  • @Api 注解用于类上,表示标识这个类是swagger的资源

  • @ApiOperation 注解用于方法,表示一个http请求的操作

  • @ApiParam 注解用于参数上,用来标明参数信息

这里返回的是JsonResult,是第02课中学习返回 json 数据时封装的实体。

启动工程,访问localhost:8080/swagger-ui.html查看Swagger页面的接口状态

img

可以看出,Swagger 页面对该接口的信息展示的非常全面,每个注解的作用以及展示的地方在上图中已经标明,通过页面即可知道该接口的所有信息,那么我们直接在线测试一下该接口返回的信息,输入id为1,看一下返回数据:

image-20220715155548161

可以看出,直接在页面返回了 json 格式的数据,开发人员可以直接使用该在线接口来测试数据的正确与否,非常方便。上面是对于单个参数的输入,如果输入参数为某个对象这种情况,Swagger 是什么样子呢?我们再写一个接口。

com/chw/course06/controller/TestController.java

@PostMapping("/insert")
@ApiOperation(value = "添加用户信息")
public JsonResult<Void> insertUser(@RequestBody @ApiParam(value = "用户信息") User user) {
    // 处理添加逻辑
    return new JsonResult<>();
}

重启项目,在浏览器中输入 localhost:8080/swagger-ui.html 看一下效果:

img

5、总结

本节课详细分析了 Swagger 的优点,以及 Spring Boot 如何集成 Swagger2,包括配置,相关注解的讲解,涉及到了实体类和接口类,以及如何使用。最后通过页面测试,体验了 Swagger 的强大之处,基本上是每个项目组中必备的工具之一,所以要掌握该工具的使用,也不难。

七、Spring Boot集成Thymeleaf模板引擎

1、Thymeleaf介绍

官方介绍:

Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。
Thymeleaf 的主要目标是为您的开发工作流程带来优雅的自然模板 - 可以在浏览器中正确显示的HTML,也可以用作静态原型,从而在开发团队中实现更强大的协作。

传统的 JSP+JSTL 组合是已经过去了,Thymeleaf 是现代服务端的模板引擎,与传统的 JSP 不同,Thymeleaf 可以使用浏览器直接打开,因为可以忽略掉拓展属性,相当于打开原生页面,给前端人员也带来一定的便利。

什么意思呢?就是说在本地环境或者有网络的环境下,Thymeleaf 均可运行。由于 thymeleaf 支持 html 原型,也支持在 html 标签里增加额外的属性来达到 “模板+数据” 的展示方式,所以美工可以直接在浏览器中查看页面效果,当服务启动后,也可以让后台开发人员查看带数据的动态页面效果。比如:

<div class="ui right aligned basic segment">
      <div class="ui orange basic label" th:text="${blog.flag}">静态原创信息</div>
</div>
<h2 class="ui center aligned header" th:text="${blog.title}">这是静态标题</h2>

类似与上面这样,在静态页面时,会展示静态信息,当服务启动后,动态获取数据库中的数据后,就可以展示动态数据,th:text 标签是用来动态替换文本的,这会在下文说明。该例子说明浏览器解释 html 时会忽略 html 中未定义的标签属性(比如 th:text),所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示数据。

2、依赖导入

在 Spring Boot 中使用 thymeleaf 模板需要引入依赖,可以在创建项目工程时勾选 Thymeleaf,也可以创建之后再手动导入,如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

另外,在html页面中如果要使用thymeleaf模板,需要在页面标签中引入名称空间

<html xmlns:th="http://www.thymeleaf.org"

3、Thymeleaf相关配置

因为 Thymeleaf 中已经有默认的配置了,我们不需要再对其做过多的配置,有一个需要注意一下,Thymeleaf 默认是开启页面缓存的,所以在开发的时候,需要关闭这个页面缓存,配置如下。

src/main/resources/application.yml

spring:
  thymeleaf:
    cache: false #关闭缓存
    prefix: classpath:/templates/ # 页面映射路径
    suffix: .html # 构建URL时附加到查看名称的后缀

否则会有缓存,导致页面没法及时看到更新后的效果。 比如你修改了一个文件,已经 update 到 tomcat 了,但刷新页面还是之前的页面,就是因为缓存引起的。

4、Thymeleaf的使用

4.1 访问静态页面

一般做网站时,都会做一个404页面和500页面,作为出错时对用户的友好提示。

Spring Boot 中会自动识别模板目录(templates/)下的404.html 和 500.html文件。

我们在templates/下新建一个error文件夹,专门防止错误的html页面,分别打印一些信息。

src/main/resources/templates/error/404.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>
<h2>欢迎来到404页面!</h2>
</body>
</html>

src/main/resources/templates/error/500.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>500</title>
</head>
<body>
<h2>欢迎来到500页面!</h2>
</body>
</html>

写一个 controller 来测试 404 和 500 页面

com/chw/course07/controller/ThymeleafController.java

package com.chw.course07.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/thymeleaf")
public class ThymeleafController {

    @RequestMapping("/test404")
    public String test404() {
        return "index";
    }

    @RequestMapping("/test500")
    public String test500() {
        int i = 1 / 0;
        return "index";
    }
}

启动项目测试。

测试失败,我的spring boot没有自动识别到404.html和500.html

这是网上找的配置类配置错误页面。https://blog.csdn.net/qq_33121395/article/details/109959412

也没有测试成功。

【注】这里有个问题需要注意一下,前面的课程中我们说了微服务中会走向前后端分离,我们在 Controller 层上都是使用的 @RestController 注解,自动会把返回的数据转成 json 格式。但是在使用模板引擎时,Controller 层就不能用 @RestController 注解了,因为在使用 thymeleaf 模板时,返回的是视图文件名,比如上面的 Controller 中是返回到 index.html 页面,如果使用 @RestController 的话,会把 index 当作 String 解析了,直接返回到页面了,而不是去找 index.html 页面,大家可以试一下。所以在使用模板时要用 @Controller 注解。

4.2 Thymeleaf中处理对象

我们来看一下 thymeleaf 模板中如何处理对象信息,假如我们在做个人博客的时候,需要给前端传博主相关信息来展示,那么我们会封装成一个博主对象,比如:

com/chw/course07/pojo/Blogger.java

public class Blogger {
    private Long id;
    private String name;
    private String pass;
    // get set 方法、构造器
}

然后在controller层中初始化一下:

com/chw/course07/controller/ThymeleafController.java

@GetMapping("/getBlogger")
public String getBlogger(Model model) {
    Blogger blogger = new Blogger(1L, "林克", "123123");
    model.addAttribute("blogger", blogger);
    return "blogger";
}

我们先初始化一个 Blogger 对象,然后将该对象放到 Model 中,然后返回到 blogger.html 页面去渲染。

接下来我们再写一个 blogger.html 来渲染 blogger 信息:

templates/blogger.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>博主信息</title>
</head>
<body>
<form action="" th:object="${blogger}">
    用户编号:<input name="id" th:value="${blogger.id}"/><br>
    用户姓名:<input type="text" name="username" th:value="${blogger.getName()}"/><br>
    登录密码:<input type="text" name="password" th:value="*{pass}"/><br>
</form>
</body>
</html>

可以看出,在 thymeleaf 模板中,使用 th:object=“${}” 来获取对象信息,然后在表单里面可以有三种方式来获取对象属性。如下:

使用 th:value="*{属性名}"
使用 th:value="${对象.属性名}",对象指的是上面使用 th:object 获取的对象
使用 th:value="${对象.get方法}",对象指的是上面使用 th:object 获取的对象

可以看出,在 Thymeleaf 中可以像写 java 一样写代码,很方便。

启动测试,然后在浏览器中输入 localhost:8080/thymeleaf/getBlogger 来测试一下数据:

image-20220715173935168

4.3 Thymeleaf中处理List

处理 List 的话,和处理上面介绍的对象差不多,但是需要在 thymeleaf 中进行遍历。我们先在 Controller 中模拟一个 List。

com/chw/course07/controller/ThymeleafController.java

@GetMapping("/getList")
public String getList(Model model) {
    Blogger blogger1 = new Blogger(1L, "塞尔达", "123123");
    Blogger blogger2 = new Blogger(2L, "林克", "321321");
    ArrayList<Blogger> list = new ArrayList<>();
    list.add(blogger1);
    list.add(blogger2);
    model.addAttribute("list", list);
    return "list";
}

写一个list.html来获取list信息,在list.html中遍历这个list

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="" th:each="blogger : ${list}"><!--遍历list,循环取出每一个赋给blogger对象-->
    用户编号:<input name="id" th:value="${blogger.id}"/><br>
    用户姓名:<input type="text" name="password" th:value="${blogger.name}"/><br>
    登录密码:<input type="text" name="username" th:value="${blogger.getPass()}"/><br>
</form>
</body>
</html>

注意,表单里面可以直接使用 ${对象.属性名} 来获取 list 中对象的属性值,也可以使用 ${对象.get方法} 来获取。但这里的这个遍历不支持 *{属性名} 来获取对象中的属性。

4.4 其他常用Thymeleaf操作

标签功能例子
th:value给属性赋值
th:style设置样式th:style=“‘display:’+@{(${sitrue}?‘none’:‘inline-block’)} + ‘’”
th:onclick点击事件th:οnclick=“‘getInfo()’”
th:if条件判断
th:href超链接Login />
th:unless条件判断(和th:if相反)Login
th:switch配合th:case
th:case配合th:switchadministator
th:src地址引入
th:action表单提交的地址

Thymeleaf还有很多其他用法。

参考Thymeleaf官方文档https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html。

5、总结

Thymeleaf 在 Spring Boot 中使用非常广泛,本节课主要分析了 thymeleaf 的优点,以及如何在 Spring Boot 中集成并使用 thymeleaf 模板,包括依赖、配置,相关数据的获取、以及一些注意事项等等。最后列举了一些 thymeleaf 中常用的标签,在实际项目中多使用,多查阅就能熟练掌握,thymeleaf 中的一些标签或者方法不用死记硬背,用到什么去查阅什么,关键是要会在 Spring Boot 中集成,用的多了就熟能生巧。

八、Spring Boot中的全局异常处理

idea模块切换至:course08-exception

在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很高,此外,开发工作量也会加大而且不好统一,这也增加了代码的维护成本。
针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单一,也实现了异常信息的统一处理和维护。同时,我们也不希望直接把异常抛给用户,应该对异常进行处理,对错误信息进行封装,然后返回一个友好的信息给用户。这节主要总结一下项目中如何使用 Spring Boot 如何拦截并处理全局的异常。

1、定义返回统一的json结构

前端或者其他服务请求本服务的接口时,该接口需要返回对应的 json 数据,一般该服务只需要返回请求着需要的参数即可,但是在实际项目中,我们需要封装更多的信息,比如状态码 code、相关信息 msg 等等,这一方面是在项目中可以有个统一的返回结构,整个项目组都适用,另一方面是方便结合全局异常处理信息,因为异常处理信息中一般我们需要把状态码和异常内容反馈给调用方。

com/chw/course08/config/JsonResult.java

package com.chw.course08.config;

public class JsonResult {
    /**
     * 异常码
     */
    protected String code;

    /**
     * 异常信息
     */
    protected String msg;

    public JsonResult() {
        this.code = "200";
        this.msg = "操作成功";
    }

    public JsonResult(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
	//get set方法
}

2、处理系统异常

新建一个GlobalExceptionHandler全局异常处理类,加上@ControllerAdvice注解即可拦截项目中抛出的异常

com/chw/course08/exception/GlobalExceptionHandler.java

package com.chw.course08.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    // 打印log,记得配置logback.xml和application.yml
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    //...
}

@ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。

@ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装数据。
Spring Boot 中,在方法上通过 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 json 结构体返回给调用者。下面举几个例子。

2.1 处理参数缺失异常

在前后端分离的架构中,前端请求后台的接口都是通过 rest 风格来调用,有时候,比如 POST 请求 需要携带一些参数,但是往往有时候参数会漏掉。另外,在微服务架构中,涉及到多个微服务之间的接口调用时,也可能出现这种情况,此时我们需要定义一个处理参数缺失异常的方法,来给前端或者调用方提示一个友好信息。

com/chw/course08/exception/GlobalExceptionHandler.java

package com.chw.course08.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    // 打印log,记得配置logback.xml和application.yml
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 缺少请求参数异常
     * @param ex HttpMessageNotReadableException
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)//改变HTTP响应的状态码
    public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {
        logger.error("缺少请求参数,{}", ex.getMessage());
        return new JsonResult("400", "缺少必要的请求参数");
    }
}

写个Controller测试一下这个异常,通过post请求方式接收两个参数:姓名和密码

com/chw/course08/controller/ExceptionController.java

package com.chw.course08.controller;
import com.chw.course08.config.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/exception")
public class ExceptionController {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @PostMapping("/test")
    public JsonResult test(@RequestParam("name") String name,
                           @RequestParam("pass") String pass) {
        logger.info("name : {}", name);
        logger.info("pass : {}", pass);
        return new JsonResult();
    }
}

用postman发送post请求测试

2.2 处理空指针异常

空指针异常是开发中司空见惯的东西了,一般发生的地方有哪些呢?
先来聊一聊一些注意的地方,比如在微服务中,经常会调用其他服务获取数据,这个数据主要是 json 格式的,但是在解析 json 的过程中,可能会有空出现,所以我们在获取某个 jsonObject 时,再通过该 jsonObject 去获取相关信息时,应该要先做非空判断。
还有一个很常见的地方就是从数据库中查询的数据,不管是查询一条记录封装在某个对象中,还是查询多条记录封装在一个 List 中,我们接下来都要去处理数据,那么就有可能出现空指针异常,因为谁也不能保证从数据库中查出来的东西就一定不为空,所以在使用数据时一定要先做非空判断。
对空指针异常的处理很简单,和上面的逻辑一样,将异常信息换掉即可。如下:
com/chw/course08/exception/GlobalExceptionHandler.java

/**
 * 空指针异常
 * @param ex NullPointerException
 * @return
 */
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
    logger.error("空指针异常 : {}", ex.getMessage());
    return new JsonResult("500", "空指针异常");
}

com/chw/course08/controller/ExceptionController.java

@RequestMapping("/testnull")
public JsonResult testNullPointException() {
    Float a = null;
    Float b = Float.intBitsToFloat(11);
    System.out.println(a + b);
    return new JsonResult();
}

用postman测试该url。

2.3 一劳永逸?

异常很多,比如还有 RuntimeException,数据库还有一些查询或者操作异常等等。由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永逸。

com/chw/course08/exception/GlobalExceptionHandler.java

/**
 * 系统异常,预期以外异常
 * @param ex
 * @return
 */
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
    logger.error("系统异常", ex);
    return new JsonResult("500", "系统发生异常,请联系管理员");
}

但是项目中,我们一般都会比较详细的去拦截一些常见异常,拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。

3、拦截自定义异常

在实际项目中,除了拦截一些系统异常外,在某些业务上,我们需要自定义一些业务异常,比如在微服务中,服务之间的相互调用很平凡,很常见。要处理一个服务的调用时,那么可能会调用失败或者调用超时等等,此时我们需要自定义一个异常,当调用失败时抛出该异常,给 GlobalExceptionHandler 去捕获。

3.1 定义异常信息

由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:

com/chw/course08/pojo/BusinessMsgEnum.java

package com.chw.course08.pojo;

public enum BusinessMsgEnum {
    /** 参数异常 */
    PARMETER_EXCEPTION("102","参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103","服务调用超时!"),
    /** 参数过大 */
    PARMETER_BIG_EXCEPTION("102","输入的图片数量不能超过50张"),
    /** 500 : 一劳永逸的提示也可以在这里定义 */
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
    //...

    /**
     * 消息码
     */
    private String code;

    /**
     * 消息内容
     */
    private String msg;

    private BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
	//get set 方法
}

3.2 拦截自定义异常

然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:

com/chw/course08/pojo/BusinessMsgEnum.java

package com.chw.course08.exception;
import com.chw.course08.pojo.BusinessMsgEnum;

public class BusinessErrorException extends RuntimeException {
    private static final long serialVersionUID = -654163543681631L;
    /**
     * 异常码
     */
    private String code;

    /**
     * 异常提示信息
     */
    private String message;

    public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
        this.code = businessMsgEnum.getCode();
        this.message = businessMsgEnum.getMsg();
    }
	//get set方法
}

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

com/chw/course08/exception/GlobalExceptionHandler.java

/**
 * 拦截业务异常,返回业务异常信息
 * @param ex
 * @return
 */
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
    String code = ex.getCode();
    String message = ex.getMessage();
    return new JsonResult(code, message);
}

在业务代码中,我们可以直接模拟一下抛出业务异常,测试一下:

com/chw/course08/controller/ExceptionController.java

@GetMapping("/business")
public JsonResult testExcpetion() {
    try {
        int i = 1 / 0;
    } catch (Exception e) {
        throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
    }
    return new JsonResult();
}

postman测试

4、总结

本节课程主要讲解了Spring Boot 的全局异常处理,包括异常信息的封装、异常信息的捕获和处理,以及在实际项目中,我们用到的自定义异常枚举类和业务异常的捕获与处理,在项目中运用的非常广泛,基本上每个项目中都需要做全局异常处理。

九、Spring Boot中的切面AOP处理

idea模块切换至course09-aop

1、什么是AOP?

之前做过笔记这里不赘述。

2、Spring Boot 中的AOP处理

2.1 AOP依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 实现AOP切面

Spring Boot 中使用 AOP 非常简单,假如我们要在项目中打印一些 log,在引入了上面的依赖之后,我们新建一个类 LogAspectHandler,用来定义切面和处理方法。只要在类上加个@Aspect注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解让该类交给 Spring 来管理。

com/chw/course09/service/LogAspectHandler.java

package com.chw.course09.service;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspectHandler {
    //...
}

介绍几个常用的注解及其使用

序号注解作用
1@Pointcut定义一个切面,即上面所描述的关注的某件事入口
2@Before在做某件事之前做的事情
3@After在做某件事之后做的事情
4@AfterReturning在做某件事之后,对其返回值做增强处理
5@AfterThrowing在做某件事抛出异常时,处理
2.2.1 @Pointcut 注解

@Pointcut 注解:用来定义一个切面(切入点),即上文中所关注的某件事情的入口。切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。

package com.chw.course09.service;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspectHandler {
    /**
     * 定义一个切面,拦截com.chw.course09.controller包和子包下的所有方法
     */
    @Pointcut("execution(* com.chw.course09.controller..*.*(..))")
    public void pointCut() {
    }
}

介绍两个常用表达式 execution() 和 annotation()

  • execution()

    • execution() 为表达式主体
      第一个 * 号的位置:表示返回值类型,* 表示所有类型
      包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.chw.course09.controller 包、子包下所有类的方法
      第二个 * 号的位置:表示类名,* 表示所有类
      *(…) :这个星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
  • annotation()

    • annotation() 方式是针对某个注解来定义切面,比如我们对具有@GetMapping注解的方法做切面,可以如下定义切面:

      @Point("@annotation(org.springframework.web.bind.annotation.GetMapping)")
      public void annotationCut(){}
      

      如果使用该切面的话,就会切入注解是 @GetMapping 的方法。因为在实际项目中,可能对于不同的注解有不同的逻辑处理,比如 @GetMapping、@PostMapping、@DeleteMapping 等。所以这种按照注解的切入方式在实际项目中也很常用。

2.2.2 @Before 注解

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 log 处理,也可以做一些信息的统计,比如获取用户的请求 url 以及用户的 ip 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如:

com/chw/course09/service/LogAspectHandler.java

package com.chw.course09.service;
import org.apache.tomcat.util.http.fileupload.RequestContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class LogAspectHandler {
    //记得配置application.yml和logback.xml
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 定义一个切面,拦截com.chw.course09.controller包和子包下的所有方法
     */
    @Pointcut("execution(* com.chw.course09.controller..*.*(..))")
    public void pointCut() {
    }

    /**
     * 在上面定义的切面方法之前执行该方法
     * @param joinPoint
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        logger.info("====进入了doBefore方法====");
        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        logger.info("即将执行方法为:{}, 属于{}包", funcName, declaringTypeName);

        //也可以用来记录一些信息,比如获取请求的url和ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求url
        String url = request.getRequestURL().toString();
        // 获取请求ip
        String ip = request.getRemoteAddr();
        logger.info("用户请求的url为:{}, ip地址为:{}", url, ip);
    }
}

JointPoint 对象很有用,可以用它来获取一个签名,然后利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等等。

2.2.3 @After 注解

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 log 处理。

com/chw/course09/service/LogAspectHandler.java

/**
 * 在上面定义的切面方法之后执行该方法
 * @param joinPoint
 */
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
    logger.info("====进入了doAfter方法====");
    Signature signature = joinPoint.getSignature();
    String method = signature.getName();
    logger.info("方法{}已执行完毕", method);
}

到这里,我们来写一个 Controller 来测试一下执行结果,新建一个 AopController 如下:

com/chw/course09/controller/AopController.java

package com.chw.course09.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }
}

启动项目访问 localhost:8080/aop/zelda 测试

控制台输出信息

INFO  c.c.c.service.LogAspectHandler - ====进入了doBefore方法====
INFO  c.c.c.service.LogAspectHandler - 即将执行方法为:testAop, 属于com.chw.course09.controller.AopController包
INFO  c.c.c.service.LogAspectHandler - 用户请求的url为:http://localhost:8080/aop/zelda, ip地址为:0:0:0:0:0:0:0:1
INFO  c.c.c.service.LogAspectHandler - ====进入了doAfter方法====
INFO  c.c.c.service.LogAspectHandler - 方法testAop已执行完毕

从打印出来的 log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before@After 两个注解的实际作用。

2.2.4 @AfterReturning 注解

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:

com/chw/course09/service/LogAspectHandler.java

/**
 * 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强。
 * @param joinPoint
 * @param result
 */
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
    Signature signature = joinPoint.getSignature();
    String classMethod = signature.getName();
    logger.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
    // 实际项目中可以根据业务做具体的返回值增强
    logger.info("对返回参数进行业务上的增强:{}", result + "增强版");
}

注意:在 @AfterReturning注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。

重启服务测试访问localhost:8080/aop/zelda

页面输出 Hello zelda

控制台输出

INFO  c.c.c.service.LogAspectHandler - ====进入了doBefore方法====
INFO  c.c.c.service.LogAspectHandler - 即将执行方法为:testAop, 属于com.chw.course09.controller.AopController包
INFO  c.c.c.service.LogAspectHandler - 用户请求的url为:http://localhost:8080/aop/zelda, ip地址为:0:0:0:0:0:0:0:1
INFO  c.c.c.service.LogAspectHandler - ====进入了doAfter方法====
INFO  c.c.c.service.LogAspectHandler - 方法testAop已执行完毕
INFO  c.c.c.service.LogAspectHandler - 方法testAop执行完毕,返回参数为:Hello zelda
INFO  c.c.c.service.LogAspectHandler - 对返回参数进行业务上的增强:Hello zelda增强版
2.2.5 @AfterThrowing 注解

顾名思义,@AfterThrowing 注解是当被切方法执行中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

com/chw/course09/service/LogAspectHandler.java

/**
 * 在上面定义的切面方法执行中抛出异常时,执行该方法
 * @param joinPoint
 * @param ex
 */
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
    Signature signature = joinPoint.getSignature();
    String method = signature.getName();
    //处理异常的逻辑
    logger.info("执行方法{}出错,异常为:{}", method, ex);
}

写一个控制方法进行测试

com/chw/course09/service/LogAspectHandler.java

@RequestMapping("/nullpoint")
public String  testNullPointException() {
    try {
        Float a = null;
        Float b = Float.intBitsToFloat(11);
        System.out.println(a + b);
    } catch (Exception e) {
        throw e;
    }
    return "success";
}

启动服务访问url测试

控制台输出

INFO  c.c.c.service.LogAspectHandler - ====进入了doBefore方法====
INFO  c.c.c.service.LogAspectHandler - 即将执行方法为:testNullPointException, 属于com.chw.course09.controller.AopController包
INFO  c.c.c.service.LogAspectHandler - 用户请求的url为:http://localhost:8080/aop/nullpoint, ip地址为:0:0:0:0:0:0:0:1
INFO  c.c.c.service.LogAspectHandler - ====进入了doAfter方法====
INFO  c.c.c.service.LogAspectHandler - 方法testNullPointException已执行完毕
INFO  c.c.c.service.LogAspectHandler - 执行方法testNullPointException出错,异常为:{}

3、总结

本节课针对 Spring Boot 中的切面 AOP 做了详细的讲解,主要介绍了 Spring Boot 中 AOP 的引入,常用注解的使用,参数的使用,以及常用 api 的介绍。AOP 在实际项目中很有用,对切面方法执行前后都可以根据具体的业务,做相应的预处理或者增强处理,同时也可以用作异常捕获处理,可以根据具体业务场景,合理去使用 AOP。

十、Spring Boot集成MyBatis

idea模块切换至course10-mybatis

1、MyBatis介绍

MyBatis 框架是一个持久层框架,是 Apache 下的顶级项目。Mybatis 可以让开发者的主要精力放在 sql 上,通过 Mybatis 提供的映射方式,自由灵活的生成满足需要的 sql 语句。使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs 映射成数据库中的记录,在国内可谓是占据了半壁江山。本节课程主要通过两种方式来对 Spring Boot 集成 MyBatis 做一讲解。重点讲解一下基于注解的方式。因为实际项目中使用注解的方式更多一点,更简洁一点,省去了很多 xml 配置(这不是绝对的,有些项目组中可能也在使用 xml 的方式)。

2、MyBatis的配置

2.1 依赖导入

<!--MyBatis依赖-->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

2.2 application.yml配置

src/main/resources/application.yml

# 服务端口
server:
  port: 8080

# 数据库地址
datasource:
  url: localhost:3306/mybatis

spring:
  datasource: # 数据库配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
    username: root
    password: 9527
    hikari: #spring boot2 之后默认的数据库连接池
      maximum-pool-size: 10 #最大连接池数
      max-lifetime: 1770000 #表示一个连接在连接池中的最大生命周期

mybatis:
  # 指定别名设置的包为所有entity
  type-aliases-package: com.chw.course10.entity
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名规范
  mapper-locations: # mapper映射文件位置
    - classpath:mapper/*.xml

3、基于xml的整合

先创建实体类

com/chw/course10/entity/User.java

package com.chw.course10.entity;

public class User {
    private Integer id;
    private String username;
    private String password;
	//构造器、get、set方法
}

写dao层的mapper接口

com/chw/course10/dao/UserMapper.java

package com.chw.course10.dao;
import com.chw.course10.entity.User;

@Mapper
public interface UserMapper {
    /**
     * 根据用户名查询用户,返回实体类
     * @param username
     * @return
     */
    User getUserByName(String username);
}

写xml文件

com/chw/course10/dao/UserMapper.java

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chw.course10.dao.UserMapper">

    <resultMap id="userResultMap" type="com.chw.course10.entity.User">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_name" jdbcType="VARCHAR" property="username"/>
        <result column="password" jdbcType="VARCHAR" property="password"/>
    </resultMap>

    <!--User getUserByName(String username);-->
    <select id="getUserByName" resultMap="userResultMap" parameterType="String">
        select * from user where user_name = #{username}
    </select>

</mapper>

写service层

com/chw/course10/service/UserService.java

package com.chw.course10.service;
import com.chw.course10.entity.User;

public interface UserService {
    User getUserByName(String name);
}

com/chw/course10/service/UserServiceImpl.java

package com.chw.course10.service;
import com.chw.course10.dao.UserMapper;
import com.chw.course10.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
    @Resource
    private UserMapper userMapper;
    
    @Override
    public User getUserByName(String name) { return userMapper.getUserByName(name); }
}

写controller层

com/chw/course10/controller/TestController.java

package com.chw.course10.controller;
import com.chw.course10.entity.User;
import com.chw.course10.service.UserService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class TestController {
    @Resource
    private UserService userService;
    
    @RequestMapping("/query/{name}")
    public User getUserByName(@PathVariable String name) {
        return userService.getUserByName(name);
    }
}

启动main方法输入url测试。

Spring Boot 如何知道这个 Mapper 呢?一种方法是在上面的 mapper 层对应的类上面添加 @Mapper 注解即可,但是这种方法有个弊端,当我们有很多个 mapper 时,那么每一个类上面都得添加 @Mapper 注解。

另一种比较简便的方法是在 Spring Boot 启动类上添加@MaperScan 注解,来扫描一个包下的所有 mapper。如下:

com/chw/course10/Course10MybatisApplication.java

package com.chw.course10;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.chw.course10.dao")
public class Course10MybatisApplication {
    public static void main(String[] args) {
        SpringApplication.run(Course10MybatisApplication.class, args);
    }
}

com.chw.course10.dao 包下的所有 mapper 都会被扫描到了。

4、基于注解的整合

基于注解的整合就不需要 *mapper.xml 配置文件了,MyBatis 主要提供了 @Select, @Insert, @Update, @Delete 四个注解。这四个注解用的非常多,也很简单,注解后面跟上对应的 sql 语句即可,举例:

com/chw/course10/dao/UserMapper.java

package com.chw.course10.dao;
import com.chw.course10.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;

public interface UserMapper {

    @Select("select * from user where id = #{id} and user_name = #{name}")
    User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);

    @Select("select * from user")
    List<User> getAll();

    //使用xml方式
    User getUserByName(String username);
}

com/chw/course10/service/UserService.java

User getUserByIdAndName(Long id, String name);
List<User> getAll();

com/chw/course10/service/UserServiceImpl.java

package com.chw.course10.service;
import com.chw.course10.dao.UserMapper;
import com.chw.course10.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService{

    @Resource
    private UserMapper userMapper;

    @Override
    public User getUserByIdAndName(Long id, String name) {
        return userMapper.getUserByIdAndName(id, name);
    }

    @Override
    public List<User> getAll() {
        return userMapper.getAll();
    }

    //使用xml方式
    @Override
    public User getUserByName(String name) {
        return userMapper.getUserByName(name);
    }
}

com/chw/course10/controller/TestController.java

package com.chw.course10.controller;
import com.chw.course10.entity.User;
import com.chw.course10.service.UserService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;

@RestController
public class TestController {
    @Resource
    private UserService userService;

    @RequestMapping("/query/{name}")
    public User getUserByName(@PathVariable String name) {
        return userService.getUserByName(name);
    }

    @RequestMapping("/queryByTwo/{id}/{name}")
    public User getUserByIdAndName(@PathVariable Long id, @PathVariable String name) {
        return userService.getUserByIdAndName(id, name);
    }

    @RequestMapping("/queryAll")
    public List<User> getAll() {
        return userService.getAll();
    }
}

启动main方法访问url测试。注意url也区分大小写。

注意,一般在设计表字段后,都会通过自动生成工具生成实体类,基本上实体类都能和表字段对应上,最起码也是驼峰对应。由于在上面配置文件中开启了驼峰的配置,所以字段都是能对的上的。

但是,万一有对应不上的话,使用 @Result 注解来解决。

com/chw/course10/dao/UserMapper.java

@Select("select * from user where id = #{id}")
@Results({
        @Result(property = "username", column = "user_name"),
        @Result(property = "password", column = "password")
})
User getUserById(Long id);

@Results 中的 @Result 注解是用来指定每一个属性和字段的对应关系。

也可以 xml 和注解相结合使用,目前我们实际的项目中也是采用混用的方式,因为有时候 xml 方便,有时候注解方便,比如就上面这个问题来说,如果我们定义了上面的这个 UserMapper.xml,那么我们完全可以使用 @ResultMap 注解来替代 @Results 注解。

src/main/resources/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chw.course10.dao.UserMapper">
    
    <resultMap id="userResultMap" type="com.chw.course10.entity.User">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_name" jdbcType="VARCHAR" property="username"/>
        <result column="password" jdbcType="VARCHAR" property="password"/>
    </resultMap>
    
</mapper>

com/chw/course10/dao/UserMapper.java

@Select("select * from user where id = #{id}")
@ResultMap("userResultMap")
User getUserById(Long id);

这种 xml 和注解结合着使用的情况也很常见,而且也减少了大量的代码,因为 xml 文件可以使用自动生成工具去生成,也不需要手动敲,所以这种使用方式也很常见。

5、总结

本节课主要系统的讲解了 Spring Boot 集成 MyBatis 的过程,分为基于 xml 形式和基于注解的形式来讲解,通过实际配置手把手讲解了 Spring Boot 中 MyBatis 的使用方式,并针对注解方式,讲解了常见的问题已经解决方式,有很强的实战意义。在实际项目中,建议根据实际情况来确定使用哪种方式,一般 xml 和注解都在用。

十一、Spring Boot事务配置管理

idea模块切换至course11-tx

1、事务相关

事务管理是 Spring Boot 框架中最为常用的功能之一,我们在实际应用开发时,基本上在 service 层处理业务逻辑的时候都要加上事务,当然了,有时候可能由于场景需要,也不用加事务(比如我们就要往一个表里插数据,相互没有影响,插多少是多少,不能因为某个数据挂了,把之前插的全部回滚)。

2、Spring Boot事务配置

2.1 依赖导入

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

导入了 mysql 依赖后,Spring Boot 会自动注入 DataSourceTransactionManager,我们不需要任何其他的配置就可以用 @Transactional 注解进行事务的使用。

mybatis 的配置,使用第10课的 mybatis 配置即可(依赖、yml配置)。

2.2 事务的测试

下面完成功能:在数据库表user中插入一条数据

iduser_namepassword
5Mipha123123

写一个插入的mapper

com/chw/course11/dao/UserMapper.java

package com.chw.course11.dao;
import org.apache.catalina.User;
import org.apache.ibatis.annotations.Insert;

public interface UserMapper {
    @Insert("insert into user (user_name, password) values (#{username},#{password})")
    Integer insertUser(User user);
}

下面测试 Spring Boot 中的事务处理,在 service 层手动抛出一个异常来模拟实际中出现的异常,然后观察事务有没有回滚,如果数据库中没有新的记录,则说明事务回滚成功。

com/chw/course11/service/UserService.java

void insertUser(User user);

com/chw/course11/service/impl/UserServiceImpl.java

package com.chw.course11.service.impl;
import com.chw.course11.dao.UserMapper;
import com.chw.course11.entity.User;
import com.chw.course11.service.UserService;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional
    public void insertUser(User user) {
        // 插入用户信息
        userMapper.insertUser(user);
        // 手动抛出异常
        throw new RuntimeException();
    }
}

main方法中添加mapper扫描

com/chw/course11/Course11TxApplication.java

package com.chw.course11;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.chw.course11.dao")
public class Course11TxApplication {
    public static void main(String[] args) {
        SpringApplication.run(Course11TxApplication.class, args);
    }
}

启动main方法,通过postman测试

localhost:8080/addUser?username=mipha&password=123123

因为在程序中抛出了个异常,会造成事务回滚,我们刷新一下数据库,并没有增加一条记录,说明事务生效了。

3、常见问题总结

针对实际项目中经常出现的,和事务相关的细节做一下总结。

3.1 异常没有被捕获

首先要说的,就是异常并没有被 ”捕获“ 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子:

@Service
public class UserServiceImpl implements UserService{    
    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional
    public void insertUser2(User user) throws Exception{
        //插入用户信息
        userMapper.insertUser(user);
        //手动抛出数据库异常
        throw new SQLException("数据库异常");
    }
}

我们看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException 来模拟实际中操作数据库发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,读者可以使用我源码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据的。

那么问题出在哪呢?因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。

@Transactional(rollbackFor = Exception.class)

3.2 异常被"吃掉"

我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有这中 try…catch,所以导致异常被 ”吃“ 掉,事务无法回滚。我们还是看上面那个例子,只不过简单修改一下代码:

@Service
public class UserServiceImpl implements UserService{    
    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertUser3(User user) throws Exception {
		try {
            //插入用户信息
            userMapper.insertUser(user);
            //手动抛出数据库异常
            throw new SQLException("数据库异常");
        } catch (Exception e) {
            //异常处理逻辑
        }
    }    
}

那这种怎么解决呢?不要用 try…catch,直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。

3.3 事务的范围

事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,该场景在这个课程中我就不模拟了,我写一个 demo 让大家看一下,把这个坑记住即可,以后在写代码时,遇到并发问题,就会注意这个坑了,那么这节课也就有价值了。

@Service
public class UserServiceImpl implements UserService{    
    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized void insertUser4(User user) throws Exception {
        // 实际中的具体业务
        userMapper.insertUser(user);
    }    
}

可以看到,因为要考虑并发问题,我在业务层代码的方法上加了个 synchronized 关键字。我举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。

但是在压测时,就会出现上面的问题,数据库中确实有两条同一用户的信息,分析其原因,在于事务的范围和锁的范围问题。

从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插入动作,导致了脏数据。

这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。

4、总结

本章主要总结了 Spring Boot 中如何使用事务,只要使用 @Transactional 注解即可使用,非常简单方便。除此之外,重点总结了三个在实际项目中可能遇到的坑点,这非常有意义,因为事务这东西不出问题还好,出了问题比较难以排查,所以总结的这三点注意事项,希望能帮助到开发中的朋友。

十二、Spring Boot中使用监听器

idea模块切换至course12-listener

image-20220718072151000

1、监听器介绍

什么是 web 监听器?web 监听器是一种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest 的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。

2、Spring Boot中监听器的使用

web 监听器的使用场景很多,比如监听 servlet 上下文用来初始化一些数据、监听 http session 用来获取当前在线的人数、监听客户端请求的 servlet request 对象来获取用户的访问信息等等。这一节中,我们主要通过这三个实际的使用场景来学习一下 Spring Boot 中监听器的使用。

2.1 监听Servlet上下文对象

监听 servlet 上下文对象可以用来初始化数据,用于缓存。什么意思呢?我举一个很常见的场景,比如用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话,用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。

针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活一点,可以再加个定时器,定期的来更新这个首页缓存。就类似与 CSDN 个人博客首页中排名的变化一样。

下面我们针对这个功能,来写一个 demo,在实际中,读者可以完全套用该代码,来实现自己项目中的相关逻辑。首先写一个 Service,模拟一下从数据库查询数据:

com/chw/course12/service/UserService.java

package com.chw.course12.service;
import com.chw.course12.entity.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public User getUser() {
        // 实际中会根据业务场景从数据库中查询相应信息
        return new User(4L, "mipha", "123123");
    }
}

com/chw/course12/listener/MyServletContextListener.java

package com.chw.course12.listener;
import com.chw.course12.entity.User;
import com.chw.course12.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;

/**
 * 使用ApplicationListener 来初始化一些数据到 application 域中的监听器
 */
@Component
public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 先获取到application上下文
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        // 获取对应的service
        UserService userService = applicationContext.getBean(UserService.class);
        User user = userService.getUser();
        // 获取application域对象,将查询到的信息放到application域中
        ServletContext application = applicationContext.getBean(ServletContext.class);
        application.setAttribute("user", user);
    }
    
}

正如注释中描述的一样,首先通过 contextRefreshedEvent 来获取 application 上下文,再通过 application 上下文来获取 UserService 这个 bean,项目中可以根据实际业务场景,也可以获取其他的 bean,然后再调用自己的业务代码获取相应的数据,最后存储到 application 域中,这样前端在请求相应数据的时候,我们就可以直接从 application 域中获取信息,减少数据库的压力。下面写一个 Controller 直接从 application 域中获取 user 信息来测试一下。

com/chw/course12/controller/TestController.java

package com.chw.course12.controller;
import com.chw.course12.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/listener")
public class TestController {

    @GetMapping("/user")
    public User getUser(HttpServletRequest request) {
        ServletContext application = request.getServletContext();
        return (User) application.getAttribute("user");
    }

}

启动项目,在浏览器中输入localhost:8080/listener/user 测试一下即可,如果正常返回 user 信息,那么说明数据已经缓存成功。不过 application 这种是缓存在内存中,对内存会有消耗,后面的课程中我会讲到 redis,到时候再给大家介绍一下 redis 的缓存。

2.2 监听HTTP会话 Session对象

监听器还有一个比较常用的地方就是用来监听 session 对象,来获取在线用户数量,现在有很多开发者都有自己的网站,监听 session 来获取当前在下用户数量是个很常见的使用场景,下面来介绍一下如何来使用。

com/chw/course12/listener/MyHttpSessionListener.java

package com.chw.course12.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

@Component
public class MyHttpSessionListener implements HttpSessionListener {
    //记得配置yml和logback.xml
    private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);

    /**
     * 记录在线的用户数量
     */
    public Integer count = 0;

    @Override
    public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
        logger.info("新用户上线了");
        count++;
        httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
    }

    @Override
    public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
        logger.info("用户下线了");
        count--;
        httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
    }
}

可以看出,首先该监听器需要实现 HttpSessionListener 接口,然后重写 sessionCreated 和 sessionDestroyed 方法,在 sessionCreated 方法中传递一个 HttpSessionEvent 对象,然后将当前 session 中的用户数量加1,sessionDestroyed 方法刚好相反,不再赘述。然后我们写一个 Controller 来测试一下。
com/chw/course12/controller/TestController.java

package com.chw.course12.controller;
import com.chw.course12.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/listener")
public class TestController {

	//省略其他控制器方法...
    
    @GetMapping("/total")
    public String getTotalUser(HttpServletRequest request) {
        Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
        return "当前在线人数:" + count;
    }
}

该 Controller 中是直接获取当前 session 中的用户数量,启动服务器,在浏览器中输入 localhost:8080/listener/total 可以看到返回的结果是1,再打开一个浏览器,请求相同的地址可以看到 count 是 2 ,这没有问题。但是如果关闭一个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是 session 销毁的方法没有执行(可以在后台控制台观察日志打印情况),当重新打开时,服务器找不到用户原来的 session,于是又重新创建了一个 session,那怎么解决该问题呢?我们可以将上面的 Controller 方法改造一下:

com/chw/course12/controller/TestController.java

@GetMapping("/total2")
public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {
    Cookie cookie;

    try {
        //把sessionId记录在浏览器中
        cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
        cookie.setPath("/");//让cookie在当前应用中可以共享
        //设置cookie有效期为2天,设置长一些
        cookie.setMaxAge(48 * 60 * 60);
        response.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
    return "当前在线人数:" + count;
}

//遗留问题

2.3 监听客户端请求Servlet Request对象

使用监听器获取用户的访问信息比较简单,实现 ServletRequestListener 接口即可,然后通过 request 对象获取一些信息。如下:

com/chw/course12/listener/MyServletRequestListener.java

package com.chw.course12.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;

/**
 * 使用ServletRequestListener获取访问信息
 */
@Component
public class MyServletRequestListener implements ServletRequestListener {

    private static Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        logger.info("request end");
        HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
        logger.info("request域中保存的name值为:{}", request.getAttribute("name"));
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
        logger.info("session id为:{}", request.getRequestedSessionId());
        logger.info("request url为:{}", request.getRequestURL());
        request.setAttribute("name", "塞尔达");
    }
}

写一个controller测试

com/chw/course12/controller/TestController.java

@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
    System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));
    return "success";
}

启动测试

3、Spring Boot中自定义事件监听

在实际项目中,我们往往需要自定义一些事件和监听器来满足业务场景,比如在微服务中会有这样的场景:微服务 A 在处理完某个逻辑之后,需要通知微服务 B 去处理另一个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景非常普遍,这个时候,我们可以自定义事件以及监听器来监听,一旦监听到微服务 A 中的某事件发生,就去通知微服务 B 处理对应的逻辑。

3.1 自定义事件

自定义事件需要继承 ApplicationEvent 对象,在事件中定义一个 User 对象来模拟数据,构造方法中将 User 对象传进来初始化。如下:

com/chw/course12/event/MyEvent.java

package com.chw.course12.event;
import com.chw.course12.entity.User;
import org.springframework.context.ApplicationEvent;

public class MyEvent extends ApplicationEvent {

    private User user;
    public MyEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
	//省略get set方法
}

3.2 自定义监听器

接下来,自定义一个监听器来监听上面定义的 MyEvent 事件,自定义监听器需要实现 ApplicationListener 接口即可。如下:

com/chw/course12/listener/MyEventListener.java

package com.chw.course12.listener;
import com.chw.course12.entity.User;
import com.chw.course12.event.MyEvent;
import org.springframework.context.ApplicationListener;

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent myEvent) {
        // 获取事件中的信息
        User user = myEvent.getUser();
        // 处理事件,实际项目中可以通知别的微服务或者处理其他逻辑
        System.out.println("用户名:" + user.getUsername());
        System.out.println("密码:" + user.getPassword());
    }
}

然后重写 onApplicationEvent 方法,将自定义的 MyEvent 事件传进来,因为该事件中,我们定义了 User 对象(该对象在实际中就是需要处理的数据,在下文来模拟),然后就可以使用该对象的信息了。

定义好了事件和监听器之后,需要手动发布事件,这样监听器才能监听到,这需要根据实际业务场景来触发,针对本文的例子,我写个触发逻辑,如下:

com/chw/course12/service/UserService.java

package com.chw.course12.service;
import com.chw.course12.entity.User;
import com.chw.course12.event.MyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class UserService {
    @Resource
    private ApplicationContext applicationContext;

    /**
     * 发布事件
     * @return
     */
    public User getUser2() {
        User user = new User(1L, "塞尔达", "123123");
        //发布事件
        MyEvent myEvent = new MyEvent(this, user);
        applicationContext.publishEvent(myEvent);
        return user;
    }
}

在 service 中注入 ApplicationContext,在业务代码处理完之后,通过 ApplicationContext 对象手动发布 MyEvent 事件,这样我们自定义的监听器就能监听到,然后处理监听器中写好的业务逻辑。

最后,在controller中写一个接口测试

com/chw/course12/controller/TestController.java

@Resource
private UserService userService;

@GetMapping("/publish")
public User publishEvent() {
    return userService.getUser2();
}

启动测试

4、总结

本课系统的介绍了监听器原理,以及在 Spring Boot 中如何使用监听器,列举了监听器的三个常用的案例,有很好的实战意义。最后讲解了项目中如何自定义事件和监听器,并结合微服务中常见的场景,给出具体的代码模型,均能运用到实际项目中去。

十三、Spring Boot中使用拦截器

idea切换至course13-interceptor

拦截器的原理很简单,是 AOP 的一种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请求。使用场景比较多的是判断用户是否有权限请求后台,更拔高一层的使用场景也有,比如拦截器可以结合 websocket 一起使用,用来拦截 websocket 请求,然后做相应的处理等等。拦截器不会拦截静态资源,Spring Boot 的默认静态目录为 resources/static,该目录下的静态页面、js、css、图片等等,不会被拦截(也要看如何实现,有些情况也会拦截,我在下文会指出)。

1、拦截器的快速使用

使用拦截器很简单,只需要两步即可:定义拦截器和配置拦截器。在配置拦截器中,Spring Boot 2.0 以后的版本和之前的版本有所不同,我会重点讲解一下这里可能出现的坑。

1.1 定义拦截器

定义拦截器,只需要实现 HandlerInterceptor 接口,HandlerInterceptor 接口是所有自定义拦截器或者 Spring Boot 提供的拦截器的鼻祖,所以,首先来了解下该接口。该接口中有三个方法: preHandle(……)、postHandle(……) 和 afterCompletion(……) 。

  • preHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在这个方法执行之前。所以 preHandle(……) 方法可以决定是否将请求放行,这是通过返回值来决定的,返回 true 则放行,返回 false 则不会向后执行。
  • postHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在执行完了该方法,但是在 DispatcherServlet 视图渲染之前。所以在这个方法中有个 ModelAndView 参数,可以在此做一些修改动作。
  • afterCompletion(……) 方法:顾名思义,该方法是在整个请求处理完成后(包括视图渲染)执行,这时做一些资源的清理工作,这个方法只有在 preHandle(……) 被成功执行后并且返回 true 才会被执行。

接下来自定义一个拦截器。

com/chw/course13/interceptor/MyInterceptor.java

package com.chw.course13.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
 * 自定义拦截器
 */
public class MyInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        String methodName = method.getName();
        logger.info("====拦截到了方法:{},在该方法执行之前执行preHandle====", methodName);
        //返回true才会继续执行,返回false则取消当前请求
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info("执行完方法之后(controller方法调用之后)执行postHandle,但此时还没有进行视图渲染");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("正在执行afterCompletion,此时整个请求处理完毕,DispatcherServlet也渲染了对应的视图,此时可以做一些清理工作");
    }
}

OK,到此为止,拦截器已经定义完成,接下来就是对该拦截器进行拦截配置。

1.2 配置拦截器

在 Spring Boot 2.0 之前,我们都是直接继承 WebMvcConfigurerAdapter 类,然后重写 addInterceptors 方法来实现拦截器的配置。但是在 Spring Boot 2.0 之后,该方法已经被废弃了(当然,也可以继续用),取而代之的是 WebMvcConfigurationSupport 方法,如下:

com/chw/course13/config/MyInterceptorConfig.java

package com.chw.course13.config;
import com.chw.course13.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }
}

在该配置中重写 addInterceptors 方法,将我们上面自定义的拦截器添加进去,addPathPatterns 方法是添加要拦截的请求,这里我们拦截所有的请求。这样就配置好拦截器了,接下来写一个 Controller 测试一下:

com/chw/course13/controller/InterceptorController.java

package com.chw.course13.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
    
    @ResponseBody
    @RequestMapping("/test")
    public String test() {
        return "hello";
    }
}

启动测试,查看控制台日志,关注执行顺序

1.3 解决静态资源被拦截问题

上文中已经介绍了拦截器的定义和配置,但是这样是否就没问题了呢?其实不然,如果使用上面这种配置的话,我们会发现一个缺陷,那就是静态资源被拦截了。可以在 resources/static/ 目录下放置一个图片资源或者 html 文件,然后启动项目直接访问,即可看到无法访问的现象。

也就是说,虽然 Spring Boot 2.0 废弃了WebMvcConfigurerAdapter,但是 WebMvcConfigurationSupport 又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开。

如何放开呢?除了在 MyInterceptorConfig 配置类中重写 addInterceptors 方法外,还需要再重写一个方法:addResourceHandlers,将静态资源放开:

com/chw/course13/config/MyInterceptorConfig.java

/**
 * 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问
 * @param registry
 */
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    super.addResourceHandlers(registry);
}

这样配置好之后,重启项目,静态资源也可以正常访问了。如果你是个善于学习或者研究的人,那肯定不会止步于此,没错,上面这种方式的确能解决静态资源无法访问的问题,但是,还有更方便的方式来配置。

我们不继承 WebMvcConfigurationSupport 类,直接实现 WebMvcConfigurer 接口,然后重写 addInterceptors 方法,将自定义的拦截器添加进去即可,如下:

com/chw/course13/config/MyInterceptorConfig.java

package com.chw.course13.config;
import com.chw.course13.interceptor.MyInterceptor;
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;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/*实现WebMvcConfigurer的配置方法*/
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 实现WebMvcConfigurer不会导致静态资源被拦截
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }
}

实现 WebMvcConfigure 接口的话,不会拦截 Spring Boot 默认的静态资源。

这两种方式都可以,具体他们之间的细节,感兴趣的读者可以做进一步的研究,由于这两种方式的不同,继承 WebMvcConfigurationSupport 类的方式可以用在前后端分离的项目中,后台不需要访问静态资源(就不需要放开静态资源了);实现 WebMvcConfigure 接口的方式可以用在非前后端分离的项目中,因为需要读取一些图片、css、js文件等等。

2、拦截器使用实例

2.1 判断用户有没有登录

一般用户登录功能我们可以这么做,要么往 session 中写一个 user,要么针对每个 user 生成一个 token,第二种要更好一点,那么针对第二种方式,如果用户登录成功了,每次请求的时候都会带上该用户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有无来判断用户有没有登录,从而实现拦截功能。我们改造一下 preHandle 方法,如下:

com/chw/course13/interceptor/MyInterceptor.java

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    String methodName = method.getName();
    logger.info("====拦截到了方法:{},在该方法执行之前执行preHandle====", methodName);

    // 判断用户有没有登录,一般登录之后用户都有一个对应的token
    String token = request.getParameter("token");
    if (null == token || "".equals(token)) {
        logger.info("用户未登录,没有权限执行...请登录");
        return false;
    }
    
    //返回true才会继续执行,返回false则取消当前请求
    return true;
}

重启项目,在浏览器中输入 localhost:8080/interceptor/test 后查看控制台日志,发现被拦截,如果在浏览器中输入 localhost:8080/interceptor/test?token=123 即可正常往下走。

2.2 取消拦截操作

根据上文,如果我要拦截所有 /admin 开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是在实际项目中,可能会有这种场景出现:某个请求也是 /admin 开头的,但是不能拦截,比如 /admin/login 等等,这样的话又需要去配置。那么,可不可以做成一个类似于开关的东西,哪里不需要拦截,我就在哪里弄个开关上去,做成这种灵活的可插拔的效果呢?

是可以的,我们可以定义一个注解,该注解专门用来取消拦截操作,如果某个 Controller 中的方法我们不需要拦截掉,即可在该方法上加上我们自定义的注解即可,下面先定义一个注解:

com/chw/course13/annotation/UnInterception.java

package com.chw.course13.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
    //@interface是注解类,不是接口。
}
/*
@Target 说明了Annotation所修饰的对象范围
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

@Retention表示声明周期
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
*/

在拦截器处理方法中添加该注解取消拦截的逻辑,如下:

com/chw/course13/interceptor/MyInterceptor.java

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    String methodName = method.getName();
    logger.info("====拦截到了方法:{},在该方法执行之前执行preHandle====", methodName);

    // 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截
    // @UnInterceptor 是我们自定义的注解
    UnInterception unInterception = method.getAnnotation(UnInterception.class);
    if (null != unInterception) {
        return true;
    }

    // 判断用户有没有登录,一般登录之后用户都有一个对应的token
    String token = request.getParameter("token");
    if (null == token || "".equals(token)) {
        logger.info("用户未登录,没有权限执行...请登录");
        return false;
    }

    //返回true才会继续执行,返回false则取消当前请求
    return true;
}

写一个controller方法测试

com/chw/course13/controller/InterceptorController.java

@UnInterception
@RequestMapping("/test2")
@ResponseBody
public String test2() {
    return "我没有被拦截";
}

启动测试 localhost:8080/interceptor/test2

3、总结

本节主要介绍了 Spring Boot 中拦截器的使用,从拦截器的创建、配置,到拦截器对静态资源的影响,都做了详细的分析。Spring Boot 2.0 之后拦截器的配置支持两种方式,可以根据实际情况选择不同的配置方式。最后结合实际中的使用,举了两个常用的场景。

十四、Spring Boot 搭建实际项目开发中的架构

从零开始搭建一个环境,主要要考虑几点:统一封装的数据结构、可调式的接口、json的处理、模板引擎的使用(本文不写该项,因为现在大部分项目都前后端分离了,但是考虑到也还有非前后端分离的项目,所以我在源代码里也加上了 thymeleaf)、持久层的集成、拦截器(这个也是可选的)和全局异常处理。一般包括这些东西的话,基本上一个 Spring Boot 项目环境就差不多了,然后就是根据具体情况来扩展了。

结合前面的课程和以上的这些点,本节课手把手带领大家搭建一个实际项目开发中可用的 Spring Boot 架构。

image-20220809160630518

0、配置日志slf4j

依赖

<!--日志依赖-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

yml配置

#日志配置
logging:
  config: D:\workspace\workspace_idea1\BootDemo\src\main\resources\logback.xml #指定项目启动时,读取哪个配置文件
  level:
    com.chw.bootdemo.mbg.mapper: trace
    #开发时设置成 trace 方便定位问题。在生产环境上,将这个日志级别再设置成 error 级别即可
    #常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG

logback.xml配置(在resources下)

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!--定义日志输出格式和存储路径-->
    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="FILE_PATH" value="D:/workspace/logs/BootDemo/demo.%d{yyyy-MM-dd}.%i.log"/>
    <!--%-5level 表示级别从左显示5个字符宽度,%logger{36} 表示 logger 名字最长36个字符,%n换行。-->
    <!--%i 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引-->

    <!--定义控制台输出-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--按照上面的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!--定义日志文件的相关参数-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按照上面配置的FILE_PATH路径来保存日志-->
            <fileNamePattern>${FILE_PATH}</fileNamePattern>
            <!--日志保存15天-->
            <maxHistory>15</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--单个日志文件的最大文件大小,超过则新建日志文件存储-->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <!--按照上面配置的LOG_PATTERN来打印日志-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!--定义日志输出级别-->
    <logger name="com.chw.bootdemo" level="INFO"/>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

1、统一的数据封装

由于封装的 json 数据的类型不确定,所以在定义统一的 json 结构时,我们需要用到泛型。

统一的 json 结构中属性包括数据、状态码、提示信息即可,构造方法可以根据实际业务需求做相应的添加即可,一般来说,应该有默认的返回结构,也应该有用户指定的返回结构。如下:

通用返回对象

package com.chw.bootdemo.entity;

import com.chw.bootdemo.exception.BusinessMsgEnum;

/**
 * @author chw1113
 * @create 2022-08-09 15:54
 */
public class JsonResult<T> {
    private T data;
    private String code;
    private String message;

    protected JsonResult() {
    }

    protected JsonResult(T data, String code, String message) {
        this.data = data;
        this.code = code;
        this.message = message;
    }

    public JsonResult(String code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 操作成功,有返回数据
     * @param data 获取的数据
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> success(T data) {
        return new JsonResult<>(data, ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
    }

    /**
     * 操作成功,没有返回数据
     * @return
     */
    public static JsonResult success() {
        return new JsonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
    }

    /**
     * 操作成功,有返回数据,自定义返回信息
     * @param data
     * @param message
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> success(T data, String message) {
        return new JsonResult<>(data, ResultCode.SUCCESS.getCode(), message);
    }

    /**
     * 操作成功,有返回数据,自定义返回的状态码和信息
     * @param data
     * @param code
     * @param message
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> success(T data, String code, String message) {
        return new JsonResult<>(data, code, message);
    }

    /**
     * 操作失败,有返回数据
     * @param data
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> failed(T data) {
        return new JsonResult<>(data, ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage());
    }

    /**
     * 操作失败,没有返回数据
     * @return
     */
    public static JsonResult failed() {
        return new JsonResult(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage());
    }

    /**
     * 操作成功,有返回数据,自定义返回信息
     * @param data
     * @param message
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> failed(T data, String message) {
        return new JsonResult<>(data, ResultCode.FAILED.getCode(), message);
    }

    /**
     * 操作失败,有返回数据,自定义返回的状态码和信息
     * @param data
     * @param code
     * @param message
     * @param <T>
     * @return
     */
    public static <T> JsonResult<T> failed(T data, String code, String message) {
        return new JsonResult<>(data, code, message);
    }

    /**
     * 使用自定义异常作为参数传递状态码和提示信息
     * @param msgEnum
     * @return
     */
    public static JsonResult selfDefineCodeAndMsg(BusinessMsgEnum msgEnum) {
        return new JsonResult(msgEnum.getCode(), msgEnum.getMessage());
    }


    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

常用操作码枚举类

package com.chw.bootdemo.entity;

/**
 * 枚举常用操作码类
 * @author chw1113
 * @create 2022-08-09 16:01
 */
public enum ResultCode {
    SUCCESS("200","操作成功"),
    FAILED("500","操作失败");
    private String code;
    private String message;

    ResultCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

异常信息枚举类

package com.chw.bootdemo.exception;

/**
 * @author chw1113
 * @create 2022-08-09 16:25
 */
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARAMETER_EXCEPTION("102","参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103","服务调用超时!"),
    /** 参数过大 */
    PARAMETER_BIG_EXCEPTION("102","参数过大"),
    /** 500 : 发生异常*/
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!"),
    /** 400 : 缺少请求参数 */
    PARAMETER_MISSING_EXCEPTION("400","缺少必要的请求参数"),
    /** 500 : 空指针异常 */
    NULLPOINTER_EXCEPTION("500","空指针异常")
    ;

    /**
     * 状态码
     */
    private String code;
    /**
     * 返回信息
     */
    private String message;

    BusinessMsgEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

2、json的处理

Json 处理工具很多,比如阿里巴巴的 fastjson,不过 fastjson 对有些未知类型的 null 无法转成空字符串,这可能是 fastjson 自身的缺陷,可扩展性也不是太好,但是使用起来方便,使用的人也蛮多的。这节课里面我们主要集成 Spring Boot 自带的 jackson。主要是对 jackson 做一下对 null 的配置即可,然后就可以在项目中使用了。

package com.chw.bootdemo.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.io.IOException;

/**
 * 将json字符串中的null转换成空字符串
 * @author chw1113
 * @create 2022-08-09 16:38
 */
@Configuration
public class JacksonConfig {
    @Bean
    @Primary //此注解是为了标识哪个Bean是默认的Bean
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            @Override
            public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString("");
            }
        });
        return objectMapper;
    }
}

这里先不测试,等下面 swagger2 配置好了之后,我们一起来测试一下。

3、swagger2在线可调试接口

有了 swagger,开发人员不需要给其他人员提供接口文档,只要告诉他们一个 Swagger 地址,即可展示在线的 API 接口文档,除此之外,调用接口的人员还可以在线测试接口数据,同样地,开发人员在开发接口时,同样也可以利用 Swagger 在线接口文档测试接口数据,这给开发人员提供了便利。使用 swagger 需要对其进行配置:

添加依赖

<!--Swagger-UI API文档生成工具-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
</dependency>

添加Swagger配置类

package com.chw.bootdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
 * swagger配置文件
 * @author chw1113
 * @create 2022-08-10 5:58
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 指定构建api文档的详细信息的方法:apiInfo()
                .apiInfo(apiInfo())
                .select()
                // 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口
                .apis(RequestHandlerSelectors.basePackage("com.chw.bootdemo.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 构建api文档的详细信息
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 设置页面标题
                .title("Spring Boot项目开发架构")
                // 设置接口描述
                .description("项目描述")
                // 设置联系方式
                .contact(new Contact("link", "url", "3084@qq.com"))
                // 设置版本
                .version("1.0")
                // 构建
                .build();
    }
}

这里测试一下,写一个controller,弄一个静态接口测试一下上面的集成内容。


localhost:8080/swagger-ui.html查看swagger页面

4、持久层集成 MyBatis

引入依赖

<!--MyBatis依赖-->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!--可选。如果不选就默认springboot的hikari连接池-->
<!--集成druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

核心配置文件

#数据库地址
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis_plus?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/shanghai
    username: root
    password: 1113
    hikari: #spring boot2 之后默认的数据库连接池
      maximum-pool-size: 10 #最大连接池数
      max-lifetime: 1770000 #表示一个连接在连接池中的最大生命周期

mybatis:
  #允许使用别名的包(别名就是首字母可小写)
  type-aliases-package: com.chw.bootdemo.model
  configuration:
    map-underscore-to-camel-case: true #驼峰命名规范
  mapper-locations: #mapper映射文件配置。如果*mapper.xml文件在java里主启动类能扫描到就不用配置,在resources里就要配置。
    - classpath:mapper/*.xml #这里的冒号后没有空格

下面配置自动生成的相关

注意自己写的mapper接口可以放在dao包里,自动生成mapper接口可以放在mapper包以做区分

引入依赖

<!--MyBatis依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--MyBatis分页插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.3</version>
</dependency>


<!-- 控制Maven在构建过程中相关配置 -->
<build>
<!-- 构建过程中用到的插件 -->
<plugins>
        <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
        <!--mybatis生成器插件-->
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.4.1</version>
            <!--mybatis生成器插件的依赖-->
            <dependencies>
                <!-- MySQL驱动 -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.22</version>
                </dependency>
                <!--生成器核心依赖-->
                <dependency>
                    <groupId>org.mybatis.generator</groupId>
                    <artifactId>mybatis-generator-core</artifactId>
                    <version>1.4.1</version>
                </dependency>
            </dependencies>
            <configuration>
                    <verbose>true</verbose>
                    <!--是否覆盖原有文件-->
                    <overwrite>true</overwrite>
                </configuration>
        </plugin>
</plugins>
</build>

generatorConfig.xml配置文件

配置相关官方文档:http://mybatis.org/generator/configreference/xmlconfig.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 数据库的连接信息 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/girls?useUnicode=true&amp;useSSL=false&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"
                        userId="root"
                        password="1113">
        </jdbcConnection>
        <!-- javaBean的生成策略-->
        <javaModelGenerator targetPackage="com.chw.bootdemo.mbg.model" targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- SQL映射文件的生成策略 -->
        <sqlMapGenerator targetPackage="com.chw.bootdemo.mbg.mapper"  targetProject=".\src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <!-- Mapper接口的生成策略 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.chw.bootdemo.mbg.mapper"  targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <!-- 逆向分析的表 -->
        <!-- tableName设置为%号,可以对应所有表,此时不写domainObjectName -->
        <!-- domainObjectName属性指定生成出来的实体类的类名 -->
        <table tableName="admin" />
        <table tableName="beauty" />
        <table tableName="boys" />
    </context>
</generatorConfiguration>

5、拦截器

拦截器在项目中使用的是非常多的(但不是绝对的),比如拦截一些置顶的 url,做一些判断和处理等等。除此之外,还需要将常用的静态页面或者 swagger 页面放行,不能将这些静态资源给拦截了。首先先自定义一个拦截器。

package com.chw.bootdemo.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author chw1113
 * @create 2022-08-10 10:15
 */
public class MyInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOGGER.info("执行方法之前执行(Controller方法调用之前。");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        LOGGER.info("执行完方法之后执行(controller方法调用之后),尚未进行视图渲染。");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LOGGER.info("整个请求都处理完,DispatcherServlet也完成了视图渲染,此时可执行清理工作。");
    }
}

然后将自定义的拦截器加入到拦截器配置中。

package com.chw.bootdemo.config;

import com.chw.bootdemo.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author chw1113
 * @create 2022-08-10 10:19
 */
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 实现WebMvcConfigurer不会导致静态资源被拦截
        registry.addInterceptor(new MyInterceptor())
                //拦截所有url
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**");
    }
}

在 Spring Boot 中,我们通常会在如下目录里存放一些静态资源:

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

上面代码中配置的 /** 是对所有 url 都进行了拦截,但我们实现了 WebMvcConfigurer 接口,不会导致 Spring Boot 对上面这些目录下的静态资源实施拦截。但是我们平时访问的 swagger 会被拦截,所以要将其放行。swagger 页面在 swagger-resources 目录下,放行该目录下所有文件即可。

然后在浏览器中输入一下 swagger 页面,若能正常显示 swagger,说明放行成功。同时可以根据后台打印的日志判断代码执行的顺序。

6、全局异常处理

全局异常处理是每个项目中必须用到的东西,在具体的异常中,我们可能会做具体的处理,但是对于没有处理的异常,一般会有一个统一的全局异常处理。在异常处理之前,最好维护一个异常提示信息枚举类,专门用来保存异常提示信息的。如下:

package com.chw.bootdemo.exception;

/**
 * @author chw1113
 * @create 2022-08-09 16:25
 */
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARAMETER_EXCEPTION("102","参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103","服务调用超时!"),
    /** 参数过大 */
    PARAMETER_BIG_EXCEPTION("102","参数过大"),
    /** 500 : 发生异常*/
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!"),
    /** 400 : 缺少请求参数 */
    PARAMETER_MISSING_EXCEPTION("400","缺少必要的请求参数"),
    /** 500 : 空指针异常 */
    NULLPOINTER_EXCEPTION("500","空指针异常")
    ;

    /**
     * 状态码
     */
    private String code;
    /**
     * 返回信息
     */
    private String message;

    BusinessMsgEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

处理系统异常

新建一个GlobalExceptionHandler全局异常处理类,加上@ControllerAdvice注解即可拦截项目中抛出的异常

package com.chw.bootdemo.exception;

import com.chw.bootdemo.entity.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * @author chw1113
 * @create 2022-08-10 10:30
 */
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 缺少请求参数异常
     * @param ex
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)//改变HTTP响应的状态码
    public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {
        LOGGER.error("缺少请求参数,{}", ex.getMessage());
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.PARAMETER_MISSING_EXCEPTION);
    }

    /**
     * 空指针异常
     * @param ex
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleTypeMismatchException(NullPointerException ex) {
        LOGGER.error("空指针异常 : {}", ex.getMessage());
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.NULLPOINTER_EXCEPTION);
    }

    /**
     * 系统异常
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        LOGGER.error("系统异常", ex);
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
    }
}

个请求都处理完,DispatcherServlet也完成了视图渲染,此时可执行清理工作。");
}
}


然后将自定义的拦截器加入到拦截器配置中。

```java
package com.chw.bootdemo.config;

import com.chw.bootdemo.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author chw1113
 * @create 2022-08-10 10:19
 */
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 实现WebMvcConfigurer不会导致静态资源被拦截
        registry.addInterceptor(new MyInterceptor())
                //拦截所有url
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**");
    }
}

在 Spring Boot 中,我们通常会在如下目录里存放一些静态资源:

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

上面代码中配置的 /** 是对所有 url 都进行了拦截,但我们实现了 WebMvcConfigurer 接口,不会导致 Spring Boot 对上面这些目录下的静态资源实施拦截。但是我们平时访问的 swagger 会被拦截,所以要将其放行。swagger 页面在 swagger-resources 目录下,放行该目录下所有文件即可。

然后在浏览器中输入一下 swagger 页面,若能正常显示 swagger,说明放行成功。同时可以根据后台打印的日志判断代码执行的顺序。

6、全局异常处理

全局异常处理是每个项目中必须用到的东西,在具体的异常中,我们可能会做具体的处理,但是对于没有处理的异常,一般会有一个统一的全局异常处理。在异常处理之前,最好维护一个异常提示信息枚举类,专门用来保存异常提示信息的。如下:

package com.chw.bootdemo.exception;

/**
 * @author chw1113
 * @create 2022-08-09 16:25
 */
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARAMETER_EXCEPTION("102","参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103","服务调用超时!"),
    /** 参数过大 */
    PARAMETER_BIG_EXCEPTION("102","参数过大"),
    /** 500 : 发生异常*/
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!"),
    /** 400 : 缺少请求参数 */
    PARAMETER_MISSING_EXCEPTION("400","缺少必要的请求参数"),
    /** 500 : 空指针异常 */
    NULLPOINTER_EXCEPTION("500","空指针异常")
    ;

    /**
     * 状态码
     */
    private String code;
    /**
     * 返回信息
     */
    private String message;

    BusinessMsgEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

处理系统异常

新建一个GlobalExceptionHandler全局异常处理类,加上@ControllerAdvice注解即可拦截项目中抛出的异常

package com.chw.bootdemo.exception;

import com.chw.bootdemo.entity.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * @author chw1113
 * @create 2022-08-10 10:30
 */
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 缺少请求参数异常
     * @param ex
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)//改变HTTP响应的状态码
    public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {
        LOGGER.error("缺少请求参数,{}", ex.getMessage());
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.PARAMETER_MISSING_EXCEPTION);
    }

    /**
     * 空指针异常
     * @param ex
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleTypeMismatchException(NullPointerException ex) {
        LOGGER.error("空指针异常 : {}", ex.getMessage());
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.NULLPOINTER_EXCEPTION);
    }

    /**
     * 系统异常
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        LOGGER.error("系统异常", ex);
        return JsonResult.selfDefineCodeAndMsg(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值