KSpringBoot整合(一)

一、为什么选择SpringBoot

01、目标

了解和掌握springboto项目



02、概述

SpringBoot是随着spring4.0诞生的,它于2014年4月,发布了SpringBoot1.0.0。
SpringBoot是一个内嵌Web容器(tomcat/jetty)的可执行程序(jar)的框架。
你在开发web应用程序的时候,不需要将项目打成war包部署到web容器中。而是作为一个可执行的程序jar即可。
启动的时候把web服务器配置好,加载起来即可运行。

传统的spring项目存在的问题

  • 大量的xml文件,配置相当繁琐
  • 整合第三方框架的配置相当复杂
  • 低效的开发效率和部署效率等问题
  • 依赖外部的web服务器
  • 日志管理需要依赖
  • 一堆的依赖在maven中pom.xml中

springboot解决了什么问题

  • 创建独立的Spring应用程序
  • 直接嵌入Tomcat、Jetty或Undertow(无需部署WAR文件)
  • 提供固执己见的“启动程序”依赖项以简化构建配置
  • 尽可能自动配置Spring和第三方库
  • 提供生产准备功能,如度量、运行状况检查和外部化配置
  • 完全没有代码生成,也不需要XML配置

帮助文档

Spring Boot Reference Documentation

二、使用SpringBoot快速搭建一个单体架构

01、目标

快速构建一个springboot项目



02、步骤

  • 1、使用idea快速构建一个springboot工程
  • 2、认识springboot的目录结构
  • 3、运行springboot项目

03、具体实现



03-01、使用idea快速构建一个springboot工程

03-02、认识springboot的目录结构

核心配置pom.xml

<!--web的依赖:包括spring,日志、json、自动配置、容器等-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--测试启动-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

核心配置applicaiton.properties

默认是空的。但是springboot提供了很多的默认值比如:
默认端口和访问路径是:8080/

核心的配置类

KuangstudyBootApplication.java 程序从这个地方进行访问和启动。

package com.kuangstudy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class KuangstudyBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(KuangstudyBootApplication.class, args);
    }
}

静态资源处理

  • staitc:存放静态资源css/js/images/fonts
  • templates:freemarker或者thymeleaf存放模板html页面的位置

03-03、运行springboot项目

  • 定义一个HelloWorldContorller.java
package com.kuangstudy.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {
    @GetMapping("/hello")
    public String test() {
        return "Hello world!!!";
    }
}
  • 直接运行:KuangstudyBootApplication.java 即可
  • 访问:http://localhost:8080/hello

三、SpringBoot项目搭建说明&starter机制

01、目标

掌握和了解springboot环境搭建和starter机制



02、分析



依赖环境

Spring Boot 2.4.6需要Java 8和兼容Java 16(包括)。Spring Framework 5.3.7或以上也是必需的。

  • maven3.5+
  • jdk1.8+
  • tomcat9.0

内置的starter

starter是一组方便的依赖项描述符,您可以将其包含在您的应用程序中。您可以一站式地获得所需的所有Spring和相关技术,而不必遍历示例代码和复制-粘贴加载依赖关系描述符。例如,如果您希望开始使用Spring和JPA访问数据库,则需要在项目中包含Spring -boot-starter-data- JPA依赖项。
启动器包含大量的依赖项,您需要这些依赖项来快速启动和运行一个项目,并且具有一组一致的、受支持的托管传递依赖项

starter的命名规范:

  • 官方的是spring-boot-starter-xxxx
  • 第三方的名的是 xxxx-boot-starter

更多从参考:Spring Boot Reference Documentation

四、SpringBoot常见配置说明

01、目标

  • 掌握和了解springboot的常见配置



02、具体实现



02-01、修改默认端口

server.port=9999
  • 修改随机端口
#随机端口
server.port=${random.int[8080,8999]}

随机端口的好处就是:
1:如果在同一台服务器上多个服务如果用同一个端口会造成端口冲突
2:在微服务项目和开发中,开发人员是不会去记住ip和端口的。我们一般在真实的开发环境下,设置一个随机端口,就不用去管理端口了。也不会造成端口的冲突。

五、SpringBoot中yml配置说明

01、目标

学习和掌握yml配置

02、分析

yml是YAML(YAML Ain’t MarkUp Language)语言的配置文件。以数据为中心,以键值的方式存在,更适合做配置文件。

  • 如果properties和yml都配置:properties的优先级要高于yml配置。
  • 如果两者不同的地方,取并集,相同的部分properties覆盖yml中的配置

03、具体实现



03-01、在src/resources目录下新建application.yml

server:
  port: 9999
alipay:
  appid: wx2e43sdf23433231111
  email: xucnengsdfs@323.com
user:
  names: zhangsan,xiaoxie,lisi,xiaoguo
  friends: zhangsan,xiaoxie,lisi,xiaoguo

注意:key和value之间,两个空格或者用tab键即可。

六、自定义配置&@Value注入属性

01、目标

使用@Value的方式完成属性的注入和配置

02、具体实现

第一:在appliation.properties中自定义属性配置如下:

# 普通属性
alipay.appid=24sdfkja23423
alipay.email=xuchengfeifei@q163.com
# 数组
user.names=zhangsan,xiaoxie,lisi,xiaoguo
# 集合 List<String>
user.friends=zhangsan,xiaoxie,lisi,xiaoguo

第二:定义属性配置类

提示:在任意配置类,springioc容器管理的类都可以通过@Value获取配置文件中的属性,如下:

package com.kuangstudy.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;

@Component
public class KsdWexinPayConfig {
    @Value("${server.port}")
    private int port;
    @Value("${alipay.appid}")
    private String appid;
    @Value("${alipay.email}")
    private String email;
    @Value("${user.names}")
    private String[] names;
    @Value("${user.friends}")
    private List<String> friends;
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public String getAppid() {
        return appid;
    }
    public void setAppid(String appid) {
        this.appid = appid;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String[] getNames() {
        return names;
    }
    public void setNames(String[] names) {
        this.names = names;
    }
    public List<String> getFriends() {
        return friends;
    }
    public void setFriends(List<String> friends) {
        this.friends = friends;
    }
}

第三:定义属性配置测试

package com.kuangstudy.controller;
import com.kuangstudy.config.KsdWexinPayConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ValueConfigurationController {
    @Autowired
    private KsdWexinPayConfig ksdWexinPayConfig;
    @GetMapping("/value")
    public KsdWexinPayConfig value() {
        return ksdWexinPayConfig;
    }
}

第四:测试访问

http://localhost:9999/value

{
    "port": 9999,
    "appid": "wx2e43sdf23433231111",
    "email": "xuchengfeifei@q163.com",
    "names": [
    "zhangsan",
    "xiaoxie",
    "lisi",
    "xiaoguo"
],
"friends": [
    "zhangsan",
    "xiaoxie",
    "lisi",
    "xiaoguo"
]
}

七、@ConfigurationProperties注入属性

01、目标

完成属性配置的注入



02、具体操作

02-01、在application.yml配置如下:

kuangstudy:
  alipay:
    key: 12352342
    appid: wx2e43sdf23433231111
    email: xuchengfeifei@163.com
    mcid: 1823923932

02-02、定义配置类:

package com.kuangstudy.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ConfigurationProperties(prefix = "kuangstudy.alipay")
@SpringBootConfiguration
@EnableConfigurationProperties(AlipayProperties.class)
public class AlipayProperties {
    // 获取支付得信息
    private String appid ;
    // 邮箱
    private String email;
    // 私钥
    private String key;
    // 商户id
    private String mcid;
    public String getAppid() {
        return appid;
    }
    public void setAppid(String appid) {
        this.appid = appid;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getMcid() {
        return mcid;
    }
    public void setMcid(String mcid) {
        this.mcid = mcid;
    }
    @Override
    public String toString() {
        return "AlipayProperties{" +
                "appid='" + appid + '\'' +
                ", email='" + email + '\'' +
                ", key='" + key + '\'' +
                ", mcid='" + mcid + '\'' +
                '}';
    }
}
@SpringBootConfiguration:申明当前是一个配置类
@ConfigurationProperties(prefix = "kuangstudy.alipay") - 标记当前类是一个属性配置类,并且自定义前缀`kuangstudy.alipay`
@EnableConfigurationProperties(AlipayProperties.class) - 注册到springboot的属性配置中,让其可以自动提示

02-03、属性配置类自动提示依赖:

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

配置好以后,记得:mvn clean package 才可以生效

02-04、访问测试:

##访问
http://localhost:9999/value2
##结果如下:
AlipayProperties{appid='wx2e43sdf23433231111', email='xuchengfeifei@163.com', key='12352342', mcid='1823923932'}

运行时会报错

 No serializer found for class org.springframework.context.expression.StandardBeanExpressionResolver......

解决方案:

SpringBoot中springMVC返回JSON报错-->Could not write JSON: No serializer found for class org.spring..._CC-lady的博客-CSDN博客用springboot简单写一个返回Model的小例子,因为控制器上注解为@RestController,自动应该返回json格式,报错:>Could not write JSON: No serializer found for class org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInthttps://blog.csdn.net/cc907566076/article/details/77644965

八、SpringBoot中使用lombok&注意事项

01、目标

掌握lombok得基本用发

02、步骤

我们编写pojo时,经常需要编写构造函数和gettersetter方法,属性多的时候,就非常浪费时间,使用lombok插件可以解决这个问题:

02-01、在idea中安装lombok插件

02-02、在pom.xml引入依赖

 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.18.20</version>
 </dependency>

02-03、然后在bean增加相关注解

  • @Data :自动提供getter和setter、hashCode、equals、toString等方法
  • @Getter:自动提供getter方法
  • @Setter:自动提供setter方法
  • @Slf4j and @Log4j:自动在bean中提供log变量,在需要打印日志的类中使用,项目中使用slf4j、log4j日志框架
  • @ToString :生成toString方法
  • @NonNull :这个注解可以用在成员方法或者构造方法的参数前面,会自动产生一个关于此参数的非空检查,如果参数为空,则抛出一个空指针异常。
  • @Cleanup 注解用于确保已分配的资源被释放(IO的连接关闭)。
  • @Synchronized 注解自动添加到同步机制,生成的代码并不是直接锁方法,而是锁代码块, 作用范围是方法上。
  • @Builder 可以实现属性连续赋值
  • @NoArgsConstructor: 自动生成无参数构造函数。
  • @AllArgsConstructor: 自动生成全参数构造函数。
  • @Value注解和@Data类似,区别在于它会把所有成员变量默认定义为private final修饰,并且不会生成set方法。
  • @Accessors @Accessors批注用于配置lombok如何生成和查找getter和setter。标注到类上,chain属性设置为true时,类的所有属性的setter方法返回值将为this,用来支持setter方法的链式写法
new User().setUsername("riemann").setPassword("123");

02-04、案例

package com.itbooking.pojo;
import lombok.*;
@ToString
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    private Integer id;
    private String bookname;
    private Long price;
    private String pic;
    private String bookdesc;
}

03、番外 - 为什么要放弃lombok

来源参考:为什么要放弃 Lombok ? - 知乎

如果您正在阅读此文,想必您对Project Lombok已经有了一段时间的了解。您是否正准备拥抱Lombok?还是正准备将如此酷炫的项目推荐给你的团队?如果您准备那么做,不妨听听我在使用Lombok一年后的一些感受。

我承认,Lombok是一个很不错的Java库,它可以让你在少写代码的同时耍耍酷,简单的几个注解,就可以干掉一大片模板代码。但是,所有的源代码很多时候是用来阅读的,只有很少的时间是用来执行的(你可以细品这句话)。一年以前,我和大多数人都认为Lombok的出现会让Java的编码体验会更好,并极力的在我的团队中推荐使用Lombok。一年以后,我开始对此产生顾虑,尤其是在我准备为开源的博客系统Una-Boot升级Java版本时,我才意识到Lombok自己掉入了一个戏法陷阱。在我进一步分析其源代码并理解相关注解的工作原理后,发现我并不需要使用一个非标准的第三方库将Java转换为一个精巧而酷炫的语言。引入Lombok让我的项目一时爽,但一时爽的代价是随着项目推进,技术债务开始累积。接下来,我将用几个大家耳熟能详的场景,重演自己是如何掉入Lombok的戏法陷阱。

爱的开始,恨的起源

面对Lombok提供的诸多“神走位”,你并不会介意在IDE上新增一个插件。对于IntelliJ IDEA玩家而言,只需搜索“Lombok Plugin”便可找到这款神器并安装上它。爱上Lombok从安装Lombok插件开始,恨也从此萌芽。

没使用Lombok之前,我们的源代码看起来是这一的:

public class MyObject{
    private Long id;
    private String name;
    private int age;
    private int gender;
    public Long getId(){
        return id;
    }
    public void setId(Long id){
        this.id = id;
    }
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public int getAge(){
        return age;
    }
    public void setAge(int age){
        this.age = age;
    }
    public int getGender(){
        return gender;
    }
    public void setGender(int gender){
        this.gender = gender;
    }
    @Override
    public boolean equals(Object o){
        if(this == o){
            return true;
        }
        if(o == null || getClass() != o.getClass()){
            return false;
        }
        MyObject obj = (MyObject) o;
        return age = obj.age &&
            gender = obj.gender &&
            Objects.equals(id,obj.id) &&
            Objects.queals(name,obj.name);
    }
    @Override
    public int hashCode(){
        return Objects.hash(id,name,age,gender);
    }
    @Override
    public String toString(){
        return "MyObject{"+
            "id="+id+
            "name="+name+
            "age="+age+
            "gender="+gander+
            "}";
    }
}

每个JavaBean都会充斥着如上述getter,setter,equals,hashCode和toString的模板代码,这看起来像一个偏胖的人(不得不承认Java是一个有缺陷的编程语言)。当我们安装好Lombok插件后,IDE便可以识别其酷炫的注解,使用Lombok的@Getter@Setter注解后,代码会像下面这样看起来很苗条:

@Getter
@Setter
public class MyObject{
    private Long id;
    private String name;
    private int age;
    private int gender;
    @Override
    public boolean equals(Object o){
        if(this == o){
            return true;
        }
        if(o == null || getClass() != o.getClass()){
            return false;
        }
        MyObject obj = (MyObject) o;
        return age = obj.age &&
            gender = obj.gender &&
            Objects.equals(id,obj.id) &&
            Objects.queals(name,obj.name);
    }
    @Override
    public int hashCode(){
        return Objects.hash(id,name,age,gender);
    }
    @Override
    public String toString(){
        return "MyObject{"+
            "id="+id+
            "name="+name+
            "age="+age+
            "gender="+gander+
            "}";
    }
}

现在的代码是否看起来爽多了?但这还不是最爽的时候。既然其他方法都替换掉了,那把toString方法也一起拿掉吧.如你所愿,可以使用@ToString注解去掉对于的方法:

@Getter
@Setter
@EqualsAndHashCode
public class MyObject{
    private Long id;
    private String name;
    private int age;
    private int gender;
    @Override
    public String toString(){
        return "MyObject{"+
            "id="+id+
            "name="+name+
            "age="+age+
            "gender="+gander+
            "}";
    }
}

经过Lombok的戏法之后,相比一开始的代码,看起来是不是很酷炫,很苗条,很性感?你以为到此为止了?远不止于此。你会发现类名上一大坨注解看起来好别扭,Lombok提供了一个组合注解@Data,可以替换掉类名头上那坨像翔一样的东西:

@Data
public class MyObject{
    private Long id;
    private String name;
    private int age;
    private int gender;
}

现在,Lombok是否让你的对象成为了你心目中完美的样子?魔鬼的“身材”,酷炫精炼。Lombok还有其他一些注解,如@Slf4j@NoArgsConstructor@AllArgsConstructor等等,介绍Lombok用法不是本文重点。

以上代码行数的变化过程,也许是无数程序员爱上Lombok的主要原因吧,这就像一个肥胖的人逐渐变成一个身材苗条的人。同时也让你看到了一个现象:你以为程序员很懒吗?其他有些时候他们比你想象中的还要懒。在爽的同时,也为代码种下了祸根。

扭曲的审美,爱的隐患

扭曲的审美,导致了被审视的对象处于亚健康状态。使用Lombok插件之后,我们的代码也处于“亚健康”状态。还是回归一开始的那句话:所有的源代码很多时候是用来阅读的,只有很少的时间是用来执行的。本质上讲,我们都追求减少程序中的样板代码以使其代码更精炼简洁,从而提高代码的可读性和可维护性。但Lombok并没有达到我们所追求的这一愿景,它仅仅是利用Java语言在编译时的空档期,使用一种很取巧的方式,将我们所需要的方法注入(写入)到当前的类中,这种过程很像在hack我们的代码,只是一种看起来酷炫的把戏。这种把戏并不智能和安全,反而会破坏Java代码现有的特性以及代码的可读性。下面,结合我自己使用Lombok之后的感受,谈谈Lombok带来的几大痛点。



03-01、JDK版本问题

当我想要将现有项目的JDK从Java 8升级到Java 11时,我发现Lombok不能正常工作了。于是我不得不将所有的Lombok注解从项目源代码中清除,并使用IDE自带的功能生成getter/setter,equals,hashCode,toString以及构造器等方法,你也可以使用Delombok工具完成这一过程。但这终究会消耗你很多的时间。



03-02、胁迫使用

当你的源代码中使用了Lombok,恰好你的代码又被其他的人所使用,那么依赖你代码的人,也必须安装Lombok插件(不管他们喜不喜欢),同时还要花费时间去了解Lombok注解的使用情况,如果不那么做,代码将无法正常运行。使用过Lombok之后,我发现这是一种很流氓的行为。



03-03、可读性差

Lombok隐藏了JavaBean封装的细节,如果你使用@AllArgsConstructor注解,它将提供一个巨型构造器,让外界有机会在初始化对象时修改类中所有的属性。首先,这是极其不安全的,因为类中某系属性我们是不希望被修改的;另外,如果某个类中有几十个属性存在,就会有一个包含几十个参数的构造器被Lombok注入到类中,这是不理智的行为;其次,构造器参数的顺序完全由Lombok所控制,我们并不能操控,只有当你需要调试时才发现有一个奇怪的“小强”在等着你;最后,在运行代码之前,所有JavaBean中的方法你只能想象他们长什么样子,你并不能看见。



03-04、代码耦合度增加

当你使用Lombok来编写某一个模块的代码后,其余依赖此模块的其他代码都需要引入Lombok依赖,同时还需要在IDE中安装Lombok的插件。虽然Lombok的依赖包并不大,但就因为其中一个地方使用了Lombok,其余所有的依赖方都要强制加入Lombok的Jar包,这是一种入侵式的耦合,如果再遇上JDK版本问题,这将是一场灾难。



03-05、得不偿失

使用Lombok,一时觉得很爽,但它却污染了你的代码,破坏了Java代码的完整性,可读性和安全性,同时还增加的团队的技术债务,这是一种弊大于利,得不偿失的操作。如果你确实想让自己的代码更加精炼,同时又兼顾可读性和编码效率,不妨使用主流的Scala或Kotlin这一基于JVM的语言。

03-06、总结

Lombok本身是一个优秀的Java代码库,它采用了一种取巧的语法糖,简化了Java的编码,为Java代码的精简提供了一种方式,但在使用此代码库时,需要了解到Lombok并非一个标准的Java库。使用Lombok,会增加团队的技术债务,降低代码的可读性,增大代码的耦合度和调式难度。虽然在一定程度上Lombok减少了样板代码的书写,但也带来了一些未知的风险。如果你正在参与一个团队项目(或大型项目),考虑到后续的升级与扩展,是否使用Lombok,请与你的团队多沟通和三思。

九、SpringBoot日志存储路径和设置日志格式

01、目标

学习springboot日志的框架,以及日志级别、设置日志的存储路径、设置日志的格式等等。

02、分析

在springboot的底层日志结构中对应:spring-boot-starter-logging可以看出,它依赖了三个框架分别是:

  • sl4j
  • logback
  • log4j

sl4j、logback和log4j三者的关系 

1、logbck和log4j是日志实现框架,就是实现怎么记录日志的。
2、sl4j:提供了java所有的日志框架的简单抽象(使用了日志的门面设计模式),说白了就是一个日志API(没有实现类), 它不能单独使用:必须要结合logback和log4j日志框架来实现结合使用。

04、springboot的日志搭配

springboot2.x以后默认采用了:sl4j+logback的日志搭配。
在开发过程中,我们可以采用sl4j的api去记录日志,底层的实现就是根据配置来决定使用logback还是log4j日志框架。

package com.kuangstudy.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @description:
 * @author: xuke
 * @time: 2021/6/1 19:58
 */
@RestController
public class LogController {
    private static final Logger log = LoggerFactory.getLogger(LogController.class);
    @GetMapping("/log")
    public void console(){
        log.trace("----------trace--------");
        log.debug("----------debug--------");
        log.info("----------info--------");
        log.warn("----------warn--------");
        log.error("----------error--------");
    }
}

通过浏览器访问:http://localhost:9999/log 只输入了如下内容:

 [nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------info--------
 [nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------warn--------
 [nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------error--------

为什么值打印了:infowarnerror呢?
因为:springboot默认的日志级别是:info

04-01、修改springboot的日志级别

打开application.yml配置文件:

# 打开com.kuangstudy目录的日志级别
logging:
  level:
    com:
      kuangstudy: trace

打印日志结果如下:

[nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------trace--------
[nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------debug--------
[nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------info--------
[nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------warn--------
[nio-9999-exec-1] com.kuangstudy.controller.LogController  : ----------error--------

日志级别:总共有TRACE < DEBUG < INFO < WARN < ERROR < FATAL ,且级别是逐渐提供,如果日志级别设置为INFO,则意味TRACE和DEBUG级别的日志都看不到。

04-02、日志输出和日志格式

server:
  port: 9999
logging:
  level:
    root: info
  file:
    # 默认情况下:会在项目的根目录下生成/output/logs/spring.log,默认的日志名是:spring.log
    path: output/logs
    # 如果不想把日志存放在logging.file.path目录下,可以采用name来重新定义存储的位置和日志文件的名称
    #name: d:/output/logs/console.log
  pattern:
    console: "%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger{50}- %msg%n"
    file: "%d{yyyy/MM/dd-HH:mm:ss} ---- [%thread] %-5level %logger{50}- %msg%n"
%c 输出logger名称
%C 输出类名
%d{HH:mm:ss.SSS} 表示输出到毫秒的时间
%t 输出当前线程名称
%-5level 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0
%logger 输出logger名称,因为Root Logger没有名称,所以没有输出
%msg 日志文本
%n 换行
其他常用的占位符有:
%F 输出所在的类文件名,如Log4j2Test.java
%L 输出行号
%M或%method 输出所在方法名
%l 输出完整的错误位置, 包括类名、方法名、文件名、行数
%p 该条日志的优先级
%replace{pattern}{regex}{substitution} 将pattern的输出结果pattern按照正则表达式regex替换成substitution

十、SpringBoot中的异步处理框架@Async

01、目标

掌握和了解springboot的异步处理和@Async

02、为什么使用异步框架

在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。

  • 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
  • 第二个原因:提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。

03、代码实现

在SpringBoot中使用@Async注解即可实现异步调用。如下:

03-01、定义异步调用任务类

package com.kuangstudy.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RegSyncService {
    @Async
    public void sendMsg(){
        // todo :模拟耗时5秒
        try {
            Thread.sleep(5000);
            log.info("---------------发送消息--------");
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
    @Async
    public void addScore(){
        // todo :模拟耗时5秒
        try {
            Thread.sleep(5000);
            log.info("---------------处理积分--------");
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

03-02、开启异步线程的支持

package com.kuangstudy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync //开启异步执行
public class KuangstudyBootHelloApplication {
    public static void main(String[] args) {
        SpringApplication.run(KuangstudyBootHelloApplication.class, args);
    }
}

03-03、定义业务执行

package com.kuangstudy.controller.reg;
import com.kuangstudy.service.RegSyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @description:
 * @author: xuke
 * @time: 2021/6/1 21:17
 */
@RestController
@Slf4j
public class RegController {
    @Autowired
    RegSyncService regSyncService;
    @GetMapping("/sync1")
    public String reguser1(){
        log.info("---------注册用户---------");
        // 异步发送消息
        regSyncService.sendMsg();
        return "ok";
    }
    @GetMapping("/sync2")
    public String reguser2(){
        log.info("---------注册用户---------");
        // 异步添加积分
        regSyncService.addScore();
        return "ok";
    }
}

测试结果:
访问:http://localhost:9999/sync1
访问:http://localhost:9999/sync2
无论访问上面那个链接,都很快就响应注册结果,同时过5秒以后执行对应的发送消息和添加积分的业务。后台的日志中也很清晰的看到,是重新开启了新的线程执行对应的任务。

04、给@Async自定义一个线程池

@Async注解默认情况下用的是SimpleAsyncTaskExecutor线程池。该线程池不是真正意义上的线程池,因为线程不重用,每次调用都会新建一个新的线程。通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。
@Async注解异步框架提供多种线程机制:

  • SimpleAsyncTaskExecutor:简单的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  • SyncTaskExecutor:这个类没实现异步调用,只是一个同步操作,只适合用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用.。
  • ThreadPoolTaskScheduler:可以和cron表达式使用。
  • ThreadPoolTaskExecutor:最常用,推荐,其本质就是:java.util.concurrent.ThreadPoolExecutor的包装
package com.kuangstudy.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@SpringBootConfiguration
public class SyncThreadPoolConfiguration {
//    @Primary
    @Bean(name="threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        // 1: 创建核心线程数
        threadPoolTaskExecutor.setCorePoolSize(10);
        // 2:线程池维护线程的最大数量,只有在缓存队列满了之后才会申请超过核心线程数的线程
        threadPoolTaskExecutor.setMaxPoolSize(100);
        // 3:缓存队列
        threadPoolTaskExecutor.setQueueCapacity(50);
        // 4:线程的空闲事件,当超过了核心线程数之外的线程在达到指定的空闲时间会被销毁
        threadPoolTaskExecutor.setKeepAliveSeconds(200);
        // 5:异步方法内部线的名称
        threadPoolTaskExecutor.setThreadNamePrefix("ksdsysn-thread-");
        // 6:缓存队列的策略
        /* 当线程的任务缓存队列已满并且线程池中的线程数量已经达到了最大连接数,如果还有任务来就会采取拒绝策略,
         * 通常有四种策略:
         *ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常:RejectedExcutionException异常
         *ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
         *ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         *ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用execute()方法,直到成功。
        * */
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

 05、小结和思考

在想啥的互联网中开发项目,针对高并发的请求,一般的做法是高并发接口异步线程池隔离处理:
比如:修改用户信息接口,刷新用户redis的缓存
比如:修改文章信息,同步redis缓存和索引库
等,都可以实现异步+线程池的方式很多的解决和达到高性能的效果。

十一、SpringBoot和在线文档Swagger的整合

01、目标

掌握swagger的配置以及相关参数命令

02、概述

相信无论是前端还是后端开发,都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。其实无论是前端调用后端,还是后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样,经常会抱怨别人写的代码没有写注释,然而自己写起代码起来,最讨厌的,也是写注释。所以仅仅只通过强制来规范大家是不够的,随着时间推移,版本迭代,接口文档往往很容易就跟不上代码了。

Swagger是什么?它能干什么?

发现了痛点就要去找解决方案。解决方案用的人多了,就成了标准的规范,这就是Swagger的由来。通过这套规范,你只需要按照它的规范去定义接口及接口相关的信息。再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,生成多种语言的客户端和服务端的代码,以及在线接口调试页面等等。这样,如果按照新的开发模式,在开发新版本或者迭代版本的时候,只需要更新Swagger描述文件,就可以自动生成接口文档和客户端服务端代码,做到调用端代码、服务端代码以及接口文档的一致性。

但即便如此,对于许多开发来说,编写这个yml或json格式的描述文件,本身也是有一定负担的工作,特别是在后面持续迭代开发的时候,往往会忽略更新这个描述文件,直接更改代码。久而久之,这个描述文件也和实际项目渐行渐远,基于该描述文件生成的接口文档也失去了参考意义。所以作为Java届服务端的大一统框架Spring,迅速将Swagger规范纳入自身的标准,建立了Spring-swagger项目,后面改成了现在的Springfox。通过在项目中引入Springfox,可以扫描相关的代码,生成该描述文件,进而生成与代码一致的接口文档和客户端代码。这种通过代码生成接口文档的形式,在后面需求持续迭代的项目中,显得尤为重要和高效。

03、具体实现

03-01、引入swagger对应依赖

<!-- Swagger -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<!-- 文档 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    <exclusions>
        <exclusion>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.5.21</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.8.5</version>
</dependency>

03-02、定义和开启swagger的配置类


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

@SpringBootConfiguration
@EnableSwagger2
public class SwaggerConfiguration {
    /**
     * 在完成上述配置之后,其实就已经可以产生帮助文档了,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生。
     * 对用户体验不好,我们通常需要自己增加一些说明来丰富文档内容。如果:
     * 加入
     *
     * @ApiIgnore 忽略暴露的 api
     * @ApiOperation(value = "查找", notes = "根据用户 ID 查找用户")
     * 添加说明
     * <p>
     * <p>
     * 其他注解:
     * @Api :用在类上,说明该类的作用
     * @ApiImplicitParams :用在方法上包含一组参数说明
     * @ApiResponses :用于表示一组响应
     * 完成上述之后,启动springboot程序,
     * 旧访问:http://localhost:8080/swagger-ui.html
     * 新访问:http://localhost:8080/doc.html
     * @ApiOperation() 用于方法;表示一个http请求的操作
     * value用于方法描述
     * notes用于提示内容
     * tags可以重新分组(视情况而用)
     * @ApiParam() 用于方法,参数,字段说明;表示对参数的添加元数据(说明或是否必填等)
     * name–参数名
     * value–参数说明
     * required–是否必填
     * @ApiModel()用于类 ;表示对类进行说明,用于参数用实体类接收
     * value–表示对象名
     * description–描述
     * 都可省略
     * @ApiModelProperty()用于方法,字段; 表示对model属性的说明或者数据操作更改
     * value–字段说明
     * name–重写属性名字
     * dataType–重写属性类型
     * required–是否必填
     * example–举例说明
     * hidden–隐藏
     * @ApiIgnore()用于类或者方法上,可以不被swagger显示在页面上 比较简单, 这里不做举例
     * @ApiImplicitParam() 用于方法
     * 表示单独的请求参数
     * @ApiImplicitParams() 用于方法,包含多个 @ApiImplicitParam
     * name–参数ming
     * value–参数说明
     * dataType–数据类型
     * paramType–参数类型
     * example–举例说明
     */
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(getApiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.kuangstudy.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo getApiInfo() {
        return new ApiInfoBuilder()
                .title("学相伴APP项目数据接口")
                .description("学相伴APP项目数据接口,在线体验文档")
                .termsOfServiceUrl("https://api.kuangstudy.com/api")
                .contact("徐柯,阿超,狂神")
                .version("1.0")
                .build();
    }
}

03-03、开始体验swagger

旧版本:http://localhost:9999/swagger-ui.html 

尝试运行成功!!!

03-04、体验接口的执行

定义用户保存接口:

package com.kuangstudy.controller.result;
import com.kuangstudy.entity.User;
import com.kuangstudy.vo.R;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    @ApiOperation("保存用户信息")
    @PostMapping("/save")
    public R saveUser() {
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setNickname("学相伴用户_" + i);
            user.setPassword("123456" + i);
            user.setSex(i % 2);
            userList.add(user);
        }
        return R.ok().data("userList", userList);
    }
}

04、总结

其实归根到底,使用Swagger,就是把相关的信息存储在它定义的描述文件里面(yml或json格式),再通过维护这个描述文件可以去更新接口文档,以及生成各端代码。而Springfox-swagger,则可以通过扫描代码去生成这个描述文件,连描述文件都不需要再去维护了。所有的信息,都在代码里面了。代码即接口文档,接口文档即代码。

更多注解参考:Swagger2 注解说明-KuangStudy-文章

十二、SpringBoot统一接口返回的标准格式R.java

01、目标

完成统一返回以及统一返回的意义和项目开发的作用

02、具体实现

02-01、定义统一返回类R

package com.kuangstudy.common.base;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
 * @ClassName BaseController
 * @Author :xuke
 * @Date :2021/1/17 13:01
 * @Description:通用数据返回格式
 * @Version: 1.0
 */
@Data
@ToString
public class R implements Serializable {
    private static final long serialVersionUID = 986823857621547280L;
    private Boolean success;
    private Integer code;
    private String message;
    private Object data;
    private R() {
    }
    public static R ok() {
        R r = new R();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }
    public static R ok(Object data) {
        R r = new R();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        r.setData(data);
        return r;
    }
    public static R error() {
        R r = new R();
        r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
        return r;
    }
    public static R setResult(ResultCodeEnum resultCodeEnum) {
        R r = new R();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }
    public R success(Boolean success) {
        this.setSuccess(success);
        return this;
    }
    public R message(String message) {
        this.setMessage(message);
        return this;
    }
    public R code(Integer code) {
        this.setCode(code);
        return this;
    }
    public R data(Object o) {
        this.setData(o);
        return this;
    }
}

02-02、定义枚举

package com.kuangstudy.ksdstate.vo;
import lombok.Getter;
@Getter
public enum ResultCodeEnum {
    SUCCESS(true, 20000,"成功"),
    UNKNOWN_REASON(false, 20001, "未知错误"),
    BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"),
    JSON_PARSE_ERROR(false, 21002, "json解析异常"),
    PARAM_ERROR(false, 21003, "参数不正确");
    private Boolean success;
    private Integer code;
    private String message;
    private ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

02-03、具体使用

@GetMapping("/user")
public R getUser() {
    return R.ok().data("1").message("xxxxx").code(133);
}

03、优化和改进

看到这里,应该很多同学都应该觉得这样的封装还不错,甚至会觉得很完美。
但是其实这个是很大的问题和弊端,因为你每写一个接口或者方法,都需要进行R返回,还是蛮累的。
如果你写这种代码在公司里推广给整个公司的人去使用,估计会被吐槽。原因开发太过于强制和依赖R类。并不是什么好事,怎么优化和处理呢?
可以使用spring提供的结果拦截增强处理机制来解决这个问题,如下:
采用springboot提供的ResponseBodyAdvicel处理即可。

ResponseBodyAdvice的作用是:拦截Controller方法的返回值,统一处理返回值到响应体中,一般来做response的统一格式,加密,签名等。如下:

  • 在使用的时候在controller的包上增加basePackages=”com.kuangstudy”,因为如果不加的话,可能给整个系统的产生冲突影响比如:如果你使用了swagger时会出现空白异常。
package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.R;
import com.kuangstudy.utils.JsonUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice(basePackages = "com.kuangstudy")
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
    /**
     * 是否支持advice功能,true是支持 false是不支持
     *
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof ErrorHandler) {
            ErrorHandler errorHandler = (ErrorHandler) o;
            return R.error().code(errorHandler.getStatus()).message(errorHandler.getMessage());
        } else if (o instanceof String) { //因为如果返回是string的话默认会调用string的处理器会直接返回,所以要进行处理
            return JsonUtil.obj2String(R.ok(o));
        }
        return R.ok(o);
    }
}
  • 因为如果返回是string的话默认会调用string的处理器直接返回所以要进行处理

这样在开发的时候,我们开发还是按照正常的开发和返回即可。不用在强迫小伙伴一定返回R类。

十三、SpringBoot封装全局异常处理器

01、目标

  • 为什么Springboot需要《全局异常处理》
  • 编码实现springboot的全局异常配置
  • 自定义异常,并集成自定义异常处理器
  • 统一返回&异常返回进行结合处理

02、分析



02-01、为什么Springboot需要《全局异常处理》

全局异常处理就是指把整个系统的异常统一自动处理,程序员可以做到不用些try/catch就能进行处理项目中出现的异常。
为什么需要全局异常处理呢?

  • 第一个原因:不用强制写try/catch,异常交由统一异常的处理机制进行捕获。
  • @GetMapping("/error1")
        public String error1() {
        int i = 1 / 0;
        return "success";
    }

    在开发中,如果不用try/catch进行捕获的话。客户端就会跳转到springboot默认的异常页面。报出500的错误信息。
    在开发中遇见了异常一般的程序开发者都会使用try/catch来进行捕获处理,如下:

  @GetMapping("/error2")
  public String error2() {
      try {
          int i = 1 / 0;
      } catch (Exception ex) {
          log.info("出现了异常:{}", ex);
          return "no";
      }
      return "success";
  }

有没有更好的处理解决方案呢?答案是:有的。可以采用全局异常处理器来解决这个问题。

  • 第二个原因:自定义异常:只能用全局异常来捕获。
@GetMapping("/error4")
public void error4() {
   throw  new RuntimeException("用户名和密码有误!!!");
}

上面的方式就没办法在try/catch进行处理。而且直接返回这些错误信息,用户是看不懂这些信息的,所以统一异常处理是非常有必要的事情!如何做呢?

  • 第三个原因:JSR303的参数验证器,参数校验不通过会抛出异常,也是无法通过try/catch进行直接捕获处理的。

02-02、编码实现springboot的全局异常配置



步骤1:统一封装异常处理枚举类

所有的运行时异常都用枚举类来进行定义即可:

package com.kuangstudy.exception;
import lombok.Getter;
/**
 * @Author xuke
 * @Description 专门处理异常
 * @Date 21:14 2021/6/25
 * @Param 
 * @return 
**/
@Getter
public enum ResultCodeEnum {
    UNKNOWN_REASON(false, 20001, "未知错误"),
    SERVER_ERROR(false, 500, "服务器忙,请稍后在试"),
    ORDER_CREATE_FAIL(false, 601, "订单下单失败");
    private Boolean success;
    private Integer code;
    private String message;
    private ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

步骤2:封装controller的异常结果处理

package com.kuangstudy.common.base;
import lombok.*;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class ErrorHandler {
    // 异常的状态码,从枚举中获得
    private Integer status;
    // 异常的消息,写用户看得懂的异常,从枚举中得到
    private String message;
    // 异常的名字
    private String exception;
    /**
     * 对异常处理进行统一封装
     *
     * @param resultCodeEnum
     * @param throwable
     * @param message
     * @return
     */
    public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable, String message) {
        ErrorHandler errorHandler = ErrorHandler.fail(resultCodeEnum, throwable);
        errorHandler.setMessage(message);
        return errorHandler;
    }
    /**
     * 对异常枚举进行封装
     *
     * @param resultCodeEnum
     * @param throwable
     * @return
     */
    public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable) {
        ErrorHandler errorHandler = new ErrorHandler();
        errorHandler.setMessage(resultCodeEnum.getMessage());
        errorHandler.setStatus(resultCodeEnum.getCode());
        errorHandler.setException(throwable.getClass().getName());
        return errorHandler;
    }
}

步骤3:定义一个全局异常处理器

package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 对服务器端出现500异常进行统一处理
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
        return errorHandler;
    }
}
  • makeExcepton方法的作用是:把运行时异常封装为ErrorHandler对象进行统一捕获处理。
  • @RestControllerAdvice@ControllerAdvice它是对controller的增强扩展处理,而全局异常就是一种扩展能力之一。
  • @ExceptionHandler(Throwable.class) :统一处理某一类型异常,从而减少代码的出现异常的复杂度和重复率,
  • @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR):指定客户端收到的http状态码,这里配置的是500,就显示成500错误。不指定也是没问题的。因为返回是根据自己的枚举进行处理了。

步骤3:运行查看结果

http://localhost:9999/error1
http://localhost:9999/error2
http://localhost:9999/error3
http://localhost:9999/error4 :
执行四个请求的异常处理都是:

{
  "status": 500,
  "message": "服务器忙,请稍后在试",
  "exception": "java.lang.RuntimeException"
}

都被拦截成到全局异常处理了。访问error4并没有显示对应的自定义异常处理信息,如何进行处理呢?答案是:自定义异常统一处理

02-03、自定义异常,并集成自定义异常处理器

自定义异常的好处是:可以根据自定异常的信息快速的定位错误出现的模块以及方便记录日志文件,快速进行错误的分析和判定。

步骤1:添加自定义异常

package com.kuangstudy.config.exception;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@Data
public class BusinessException extends RuntimeException {
    private Integer code;
    private String message;
    public BusinessException(ResultCodeEnum resultCodeEnum) {
        this.code = resultCodeEnum.getCode();
        this.message = resultCodeEnum.getMessage();
    }
    public BusinessException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
package com.kuangstudy.config.exception;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@Data
public class OrderException extends RuntimeException {
    private Integer code;
    private String message;
    public OrderException(ResultCodeEnum resultCodeEnum) {
        this.code = resultCodeEnum.getCode();
        this.message = resultCodeEnum.getMessage();
    }
    public OrderException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

步骤2:添加自定义异常处理方法

package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice(basePackages = "com.kuangstudy")
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 对服务器端出现500异常进行统一处理
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
        return errorHandler;
    }
    /**
     * 对自定义异常进行统一处理
     */
    @ExceptionHandler(BusinessException.class)
    public ErrorHandler handlerBusinessException(BusinessException e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .status(e.getCode())
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("请求的地址是:{},BusinessException出现异常:{}", request.getRequestURL(), e);
        return errorHandler;
    }
    /**
     * 对自定义异常进行统一处理
     */
    @ExceptionHandler(OrderException.class)
    public ErrorHandler handlerOrderException(OrderException e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .status(e.getCode())
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("请求的地址是:{},OrderException出现异常:{}", request.getRequestURL(), e);
        return errorHandler;
    }
}

步骤3:定义方法进行测试

 @GetMapping("/error5")
 public void error5() {
     throw new BusinessException(ResultCodeEnum.LOGIN_CODE_FAIL_ERROR);
 }
@GetMapping("/error6")
public void error6() {
    throw new OrderException(ResultCodeEnum.LOGIN_PHONE_ERRROR);
}

访问:http://localhost:9999/error5

{
  "status": 20007,
  "message": "短信验证码失效,请重新发送",
  "exception": "com.kuangstudy.config.exception.BusinessException"
}

访问:http://localhost:9999/error6

{
  "status": 20002,
  "message": "手机号码不能为空",
  "exception": "com.kuangstudy.config.exception.OrderException"
}

02-04、统一返回&异常返回进行结合处理

因为显示统一异常处理和统一返回处理存在两种格式的返回,这个时候如果是接口的调用者就会出现凌乱的感觉。我们可以使用Controller的结果返回拦截处理进行结果处理在返回。

统一异常和统一结果返回结合处理:

package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.R;
import com.kuangstudy.utils.JsonUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 11:16
 */
@ControllerAdvice
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
    /**
     * 是否支持advice功能,true是支持 false是不支持
     *
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof ErrorHandler) {
            ErrorHandler errorHandler = (ErrorHandler) o;
            return R.error().code(errorHandler.getStatus()).message(errorHandler.getMessage());
        } else if (o instanceof String) {
            return JsonUtil.obj2String(R.ok(o));
        }
        return R.ok(o);
    }
}

十四、SpringBoot的参数校验器-Validator

01、目标

  • 为什么要做Validator参数校验器
  • 编码实现对用户名、密码、邮箱、手机号码、身份证等的校验器
  • 实现一个手机号码的自定义验证器
  • 把验证器和全局异常处理结合

02、分析

02-01、为什么要使用validator参数校验器

因为在日常的开发中,服务端对象的校验是非常重要的一个环节,比如:注册的时候:校验用户名,密码,身份证,邮箱等信息是否为空,以及格式是否正确,但是这种在日常的开发中进行校验太繁琐了,代码繁琐而且很多。
Validator框架应运而生,它的出现就是为了解决开发人员在开发的时候减少代码的,提升开发效率。它专门用来做接口的参数校验,比如:密码长度、是否为空等等。

spring的validator校验框架遵守的是JSR-303的验证规范(参数校验规范),JSP全称:Java Specification Requests缩写。
在默认情况下:SpringBoot会引入Hibernate Validatorr机制来支持JSR-303验证规范。

SpringBoot的validator校验框架支持如下特征:

  • JSR303特征:JSR303是一项标准,只提供规范不提供实现。规定一些校验规范即校验注解。比如:@Null@NotNull@Pattern。这些类都位于:javax.validation.constraints包下。
  • hibernate validation特征:hibernate validation是对JSR303规范的实现并且进行了增强和扩展。并增加了注解:@Email@Length@Range等等。
  • spring Validation:Spring Validation是对Hibernate Validation的二次封装。在SpringMvc模块中添加了自动校验。并将校验信息封装到特定的类中。

常用注解列表

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    
Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且trim后长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

03、实战开发

03-01、在pom.xml添加依赖

springboot天然就支持了validator数据对象校验

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

03-02、新建用户实体并结合验证注解

package com.kuangstudy.vo;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.util.Date;

@Data
public class UserVo {
    @NotNull(message = "用户id不能为空")
    private Long userId;
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过20个字符")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
    private String username;
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String mobile;
    @NotBlank(message = "联系邮箱不能为空")
    @Email(message = "邮箱格式不对")
    private String email;
    @Future(message = "时间必须是将来时间")
    private Date createTime;
}

03-03、添加UserValidationController进行校验

package com.kuangstudy.controller.validator;
import com.kuangstudy.common.base.R;
import com.kuangstudy.vo.UserVo;
import io.swagger.annotations.Api;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(description = "用户校验")
@RequestMapping("/user")
public class UserValiatorController {
    @PostMapping("/valiator/reg")
    public UserVo createUser(@RequestBody @Validated UserVo userVo) {
        return userVo;
    }
}

@Validated注解记得一定要加上,它的作用是用来校验UserVo的参数是否正确的注解。

03-04、进行校验测试

http://localhost:9999/doc.html

测试数据:

{
    "createTime": "",
    "email": "xxxxxx@qq.com",
    "mobile": "15074816437",
    "userId": 0,
    "username": "xxxx"
}

验证错误的信息

{
  "success": false, 
  "code": 500, 
  "message": "服务器忙,请稍后在试", 
  "error": "Validation failed for argument [0] in public com.kuangstudy.vo.UserVo com.kuangstudy.controller.validator.UserValiatorController.createUser(com.kuangstudy.vo.UserVo) with 2 errors: [Field error in object 'userVo' on field 'mobile': rejected value [15074816x437]; codes [Pattern.userVo.mobile,Pattern.mobile,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.mobile,mobile]; arguments []; default message [mobile],[Ljavax.validation.constraints.Pattern$Flag;@475daaf7,^[1][3,4,5,6,7,8,9][0-9]{9}$]; default message [手机号格式有误]] [Field error in object 'userVo' on field 'email': rejected value [xxxxxxqq.com]; codes [Email.userVo.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@354723d6,.*]; default message [邮箱格式不对]] ", 
  "data": null
}

如果填写正确的信息是:

{
  "success": true,
  "code": 20000,
  "message": "成功",
  "error": null,
  "data": {
    "userId": 0,
    "username": "xxxx",
    "mobile": "15074816437",
    "email": "xxxxxx@qq.com",
    "createTime": null
  }
}

但是这种信息在开发中,其实还是存在很大的不友好,怎么处理这种问题?可以结合统一异常处理即可。

04、把validator异常加入到《全局异常处理器中》

因为上述返回的异常根本没办法理解和看明白。所以对验证出现的信息返回可以结合全局异常处理器进行统一处理。
因为验证失败,springboot内部是以异常的方式进行报错和返回,这个时候我们可以捕捉这个异常进行处理

package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import com.kuangstudy.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestControllerAdvice(basePackages = "com.kuangstudy.controller")
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 对验证的统一异常进行统一处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorHandler handlerValiator(MethodArgumentNotValidException e, HttpServletRequest request) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<Map<String, String>> mapList = toValidatorMsg(fieldErrors);
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.PARAM_ERROR, e, JsonUtil.obj2String(mapList));
        errorHandler.setErrors("");
        return errorHandler;
    }
    /**
     * 对验证异常进行统一处理
     * @param fieldErrorList
     * @return
     */
    private List<Map<String, String>> toValidatorMsg(List<FieldError> fieldErrorList) {
        List<Map<String, String>> mapList = new ArrayList<>();
        for (FieldError fieldError : fieldErrorList) {
            Map<String, String> map = new HashMap<>();
            map.put("field", fieldError.getField());
            map.put("msg", fieldError.getDefaultMessage());
            mapList.add(map);
        }
        return mapList;
    }
}

测试数据

{
    "createTime": "",
    "email": "xxxxxxqq.com",
    "mobile": "1507481x6437",
    "userId": 0,
    "username": "xxxx"
}

错误的测试结果

{
  "success": false,
  "code": 21003,
  "message": "[{\"msg\":\"邮箱格式不对\",\"field\":\"email\"},{\"msg\":\"手机号格式有误\",\"field\":\"mobile\"}]",
  "error": "",
  "data": null
}

05、自定义校验器

在开发中有时候有些验证需求满足不了我们的业务,我们可以进行扩展进行处理验证、springboot的扩展validation的时候做了接口。具体实现如下:
步骤1:首先定义验证异常注解

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.kuangstudy.common.ano;
import com.kuangstudy.config.handler.PhoneValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "手机格式不正确!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Phone[] value();
    }
}

步骤2:定义具体的验证处理类

package com.kuangstudy.config.handler;
import com.kuangstudy.common.ano.Phone;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class PhoneValidator implements ConstraintValidator<Phone, String> {
    @Override
    public boolean isValid(String phonevalue, ConstraintValidatorContext constraintValidatorContext) {
        // 1: 如果用户没输入直接返回不校验,因为空的判断交给@NotNull去做就行了
        if (phonevalue == null && phonevalue.length() == 0) {
            return true;
        }
        Pattern p = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$");
        // 2:如果校验通过就返回true,否则返回false;
        Matcher matcher = p.matcher(phonevalue);
        return matcher.matches();
    }
    @Override
    public void initialize(Phone constraintAnnotation) {
    }
}

步骤3:在实体vo中使用

 @NotBlank(message = "手机号2不能为空")
 @Phone
 private String phone;

步骤4:测试结果

验证正确返回

{
  "success": true,
  "code": 20000,
  "message": "成功",
  "error": null,
  "data": {
    "userId": 0,
    "username": "xxxxxxx",
    "mobile": "13453432143",
    "phone": "13453432143x",
    "email": "xxxx@qq.com",
    "createTime": null
  }
}

验证错误返回

{
  "success": false,
  "code": 21003,
  "message": "[{\"msg\":\"手机格式不正确!\",\"field\":\"phone\"}]",
  "error": "",
  "data": null
}

十五、SpringBoot-Assert和自定义ValidatorUtils

01、目标

  • 如何使用Assert参数校验
  • 为什么用了Validator参数校验,还必须再用Assert参数校验呢?
  • 自定义属于自己的ValdatorUtils校验器

02、Assert参数校验

Assert是断言的意思,它是Spring提供的一个工具类,org.springframework.util.Assert
Web 应用在接受表单提交的数据后都需要对其进行合法性检查,如果表单数据不合法,请求将被驳回。类似的,当我们在编写类的方法时,也常常需要对方法入参进行合法性检查,如果入参不符合要求,方法将通过抛出异常的方式拒绝后续处理。

核心方法:

import java.util.Collection;
import java.util.Map;
public abstract class Assert
{
  public static void isTrue(boolean expression, String message)
  {
    if (!(expression))
      throw new IllegalArgumentException(message);
  }
  public static void isTrue(boolean expression)
  {
    isTrue(expression, "[Assertion failed] - this expression must be true");
  }
  public static void isNull(Object object, String message)
  {
    if (object != null)
      throw new IllegalArgumentException(message);
  }
  public static void isNull(Object object)
  {
    isNull(object, "[Assertion failed] - the object argument must be null");
  }
  public static void notNull(Object object, String message)
  {
    if (object == null)
      throw new IllegalArgumentException(message);
  }
  public static void notNull(Object object)
  {
    notNull(object, "[Assertion failed] - this argument is required; it must not be null");
  }
  public static void hasLength(String text, String message)
  {
    if (!(StringUtils.hasLength(text)))
      throw new IllegalArgumentException(message);
  }
  public static void hasLength(String text)
  {
    hasLength(text, 
      "[Assertion failed] - this String argument must have length; it must not be null or empty");
  }
  public static void hasText(String text, String message)
  {
    if (!(StringUtils.hasText(text)))
      throw new IllegalArgumentException(message);
  }
  public static void hasText(String text)
  {
    hasText(text, 
      "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank");
  }
  public static void doesNotContain(String textToSearch, String substring, String message)
  {
    if ((StringUtils.hasLength(textToSearch)) && (StringUtils.hasLength(substring)) && 
      (textToSearch.indexOf(substring) != -1))
      throw new IllegalArgumentException(message);
  }
  public static void doesNotContain(String textToSearch, String substring)
  {
    doesNotContain(textToSearch, substring, 
      "[Assertion failed] - this String argument must not contain the substring [" + substring + "]");
  }
  public static void notEmpty(Object[] array, String message)
  {
    if (ObjectUtils.isEmpty(array))
      throw new IllegalArgumentException(message);
  }
  public static void notEmpty(Object[] array)
  {
    notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element");
  }
  public static void noNullElements(Object[] array, String message)
  {
    if (array != null)
      for (int i = 0; i < array.length; ++i)
        if (array[i] == null)
          throw new IllegalArgumentException(message);
  }
  public static void noNullElements(Object[] array)
  {
    noNullElements(array, "[Assertion failed] - this array must not contain any null elements");
  }
  public static void notEmpty(Collection collection, String message)
  {
    if (CollectionUtils.isEmpty(collection))
      throw new IllegalArgumentException(message);
  }
  public static void notEmpty(Collection collection)
  {
    notEmpty(collection, 
      "[Assertion failed] - this collection must not be empty: it must contain at least 1 element");
  }
  public static void notEmpty(Map map, String message)
  {
    if (CollectionUtils.isEmpty(map))
      throw new IllegalArgumentException(message);
  }
  public static void notEmpty(Map map)
  {
    notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry");
  }
  public static void isInstanceOf(Class clazz, Object obj)
  {
    isInstanceOf(clazz, obj, "");
  }
  public static void isInstanceOf(Class type, Object obj, String message)
  {
    notNull(type, "Type to check against must not be null");
    if (!(type.isInstance(obj)))
      throw new IllegalArgumentException(message + 
        "Object of class [" + "null" + 
        "] must be an instance of " + type);
  }
  public static void isAssignable(Class superType, Class subType)
  {
    isAssignable(superType, subType, "");
  }
  public static void isAssignable(Class superType, Class subType, String message)
  {
    notNull(superType, "Type to check against must not be null");
    if ((subType == null) || (!(superType.isAssignableFrom(subType))))
      throw new IllegalArgumentException(message + subType + " is not assignable to " + superType);
  }
  public static void state(boolean expression, String message)
  {
    if (!(expression))
      throw new IllegalStateException(message);
  }
  public static void state(boolean expression)
  {
    state(expression, "[Assertion failed] - this state invariant must be true");
  }
}

03、为什么有Validator参数校验还需要Assert参数校验呢?

原因1:因为validator只解决了参数自身的数据校验,解决不了参数和业务数据直接的校验,比如判断一个对象是否为null
原因2:比如判断两个值是否相等不相等,如:密码和确认密码等等
比如:

新增逻辑判断方法

@PostMapping("/valiator/reg2")
public UserVo createUser() {
    //UserVo userVo = userService.getById(id);
    UserVo userVo = null;
    if (userVo == null) {
        throw new IllegalArgumentException("用户不存在!!!");
    }
    return userVo;
}
@PostMapping("/valiator/reg3")
public UserVo createUser3() {
    UserVo userVo = null;
    Assert.isNull(userVo,"Assert用户不存在!!!");
    return userVo;
}

增加统一异常拦截处理

 /**
 * assert异常校验处理
 */
 @ExceptionHandler(IllegalArgumentException.class)
 public ErrorHandler handlerIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
     ErrorHandler errorHandler = ErrorHandler.builder()
     .status(4000)
     .message(e.getMessage())
     .exception(e.getClass().getName())
     .build();
     log.error("请求的地址是:{},IllegalArgumentException出现异常:{}", request.getRequestURL(), e);
     return errorHandler;
 }

测试结果

{
  "success": false,
  "code": 4000,
  "message": "用户不存在!!!",
  "error": null,
  "data": null
}

03、自定义验证器

借鉴Assert的思想,封装属于自己的验证器

package com.kuangstudy.utils;
import org.springframework.util.Assert;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * @description: 校验工具类
 * @author: xuke
 * @time: 2020/9/26 1:19
 */
public class ValidatorUtil extends Assert {
    /**
     * @return boolean
     * @Author xuke
     * @Description 验证手机号码
     * @Date 0:33 2020/9/26
     * @Param [phone]
     **/
    public static void isValidatorPhone(String phone) {
        String regex = "(^0?1[3|4|5|7|6|8|9][0-9]\\d{8}$)";
        if (phone.length() != 11) {
            throw new IllegalArgumentException("手机号码长度必须是11位");
        }
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(phone);
        boolean isMatch = m.matches();
        if (!isMatch) {
            throw new IllegalArgumentException("请正确输入手机号码!");
        }
    }
    /**
     * 利用正则表达式判断字符串是否是数字
     *
     * @param str
     * @return
     */
    public static void isNumeric(String str) {
        Pattern pattern = Pattern.compile("[0-9]*");
        Matcher isNum = pattern.matcher(str);
        if (!isNum.matches()) {
            throw new IllegalArgumentException("请输入数字");
        }
    }
}

参考工具类

package com.kuangstudy.utils;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
/**
 * 常用的一些验证,如手机、移动号码、联通号码、电信号码、密码、座机、 邮政编码、邮箱、年龄、身份证、URL、QQ、汉字、字母、数字等
 */
public class ValidateUtil {
    /**
     * 手机号规则
     */
    public static final String MOBILE_PATTERN = "^((13[0-9])|(14[0-9])|(15[0-9])|(17[0-9])|(18[0-9]))(\\d{8})$";
    /**
     * 中国电信号码格式验证 手机段: 133,153,180,181,189,177,1700,173
     **/
    private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|53|7[37]|8[019])\\d{8}$)|(?:^(?:\\+86)?1700\\d{7}$)";
    /**
     * 中国联通号码格式验证 手机段:130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     **/
    private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[5]|5[56]|7[56]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[7-9]\\d{7}$)";
    /**
     * 中国移动号码格式验证 手机段:134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     **/
    private static final String CHINA_MOVE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[7]|5[0-27-9]|7[8]|8[2-478])\\d{8}$)|(?:^(?:\\+86)?1705\\d{7}$)";
    /**
     * 密码规则(6-16位字母、数字)
     */
    public static final String PASSWORD_PATTERN = "^[0-9A-Za-z]{6,16}$";
    /**
     * 固号(座机)规则
     */
    public static final String LANDLINE_PATTERN = "^(?:\\(\\d{3,4}\\)|\\d{3,4}-)?\\d{7,8}(?:-\\d{1,4})?$";
    /**
     * 邮政编码规则
     */
    public static final String POSTCODE_PATTERN = "[1-9]\\d{5}";
    /**
     * 邮箱规则
     */
    public static final String EMAIL_PATTERN = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
    /**
     * 年龄规则 1-120之间
     */
    public static final String AGE_PATTERN = "^(?:[1-9][0-9]?|1[01][0-9]|120)$";
    /**
     * 身份证规则
     */
    public static final String IDCARD_PATTERN = "^\\d{15}|\\d{18}$";
    /**
     * URL规则,http、www、ftp
     */
    public static final String URL_PATTERN = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
    /**
     * QQ规则
     */
    public static final String QQ_PATTERN = "^[1-9][0-9]{4,13}$";
    /**
     * 全汉字规则
     */
    public static final String CHINESE_PATTERN = "^[\u4E00-\u9FA5]+$";
    /**
     * 全字母规则
     */
    public static final String STR_ENG_PATTERN = "^[A-Za-z]+$";
    /**
     * 整数规则
     */
    public static final String INTEGER_PATTERN = "^-?[0-9]+$";
    /**
     * 正整数规则
     */
    public static final String POSITIVE_INTEGER_PATTERN = "^\\+?[1-9][0-9]*$";
    /**
     * @param mobile 手机号码
     * @return boolean
     * @Description: 验证手机号码格式
     */
    public static boolean validateMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(MOBILE_PATTERN);
    }
    /**
     * 验证是否是电信手机号,133、153、180、189、177
     *
     * @param mobile 手机号
     * @return boolean
     */
    public static boolean validateTelecom(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_TELECOM_PATTERN);
    }
    /**
     * 验证是否是联通手机号 130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     *
     * @param mobile 电话号码
     * @return boolean
     */
    public static boolean validateUnionMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_UNICOM_PATTERN);
    }
    /**
     * 验证是否是移动手机号
     *
     * @param mobile 手机号 134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     * @return boolean
     */
    public static boolean validateMoveMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_MOVE_PATTERN);
    }
    /**
     * @param pwd 密码
     * @return boolean
     * @Description: 验证密码格式  6-16 位字母、数字
     */
    public static boolean validatePwd(String pwd) {
        if (StringUtils.isEmpty(pwd)) {
            return Boolean.FALSE;
        }
        return Pattern.matches(PASSWORD_PATTERN, pwd);
    }
    /**
     * 验证座机号码,格式如:58654567,023-58654567
     *
     * @param landline 固话、座机
     * @return boolean
     */
    public static boolean validateLandLine(final String landline) {
        if (StringUtils.isEmpty(landline)) {
            return Boolean.FALSE;
        }
        return landline.matches(LANDLINE_PATTERN);
    }
    /**
     * 验证邮政编码
     *
     * @param postCode 邮政编码
     * @return boolean
     */
    public static boolean validatePostCode(final String postCode) {
        if (StringUtils.isEmpty(postCode)) {
            return Boolean.FALSE;
        }
        return postCode.matches(POSTCODE_PATTERN);
    }
    /**
     * 验证邮箱(电子邮件)
     *
     * @param email 邮箱(电子邮件)
     * @return boolean
     */
    public static boolean validateEamil(final String email) {
        if (StringUtils.isEmpty(email)) {
            return Boolean.FALSE;
        }
        return email.matches(EMAIL_PATTERN);
    }
    /**
     * 判断年龄,1-120之间
     *
     * @param age 年龄
     * @return boolean
     */
    public static boolean validateAge(final String age) {
        if (StringUtils.isEmpty(age)) {
            return Boolean.FALSE;
        }
        return age.matches(AGE_PATTERN);
    }
    /**
     * 身份证验证
     *
     * @param idCard 身份证
     * @return boolean
     */
    public static boolean validateIDCard(final String idCard) {
        if (StringUtils.isEmpty(idCard)) {
            return Boolean.FALSE;
        }
        return idCard.matches(IDCARD_PATTERN);
    }
    /**
     * URL地址验证
     *
     * @param url URL地址
     * @return boolean
     */
    public static boolean validateUrl(final String url) {
        if (StringUtils.isEmpty(url)) {
            return Boolean.FALSE;
        }
        return url.matches(URL_PATTERN);
    }
    /**
     * 验证QQ号
     *
     * @param qq QQ号
     * @return boolean
     */
    public static boolean validateQq(final String qq) {
        if (StringUtils.isEmpty(qq)) {
            return Boolean.FALSE;
        }
        return qq.matches(QQ_PATTERN);
    }
    /**
     * 验证字符串是否全是汉字
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateChinese(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(CHINESE_PATTERN);
    }
    /**
     * 判断字符串是否全字母
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateStrEnglish(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(STR_ENG_PATTERN);
    }
    /**
     * 判断是否是整数,包括负数
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateInteger(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(INTEGER_PATTERN);
    }
    /**
     * 判断是否是大于0的正整数
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validatePositiveInt(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(POSITIVE_INTEGER_PATTERN);
    }
}

狂神说java:首页-KuangStudy

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值