SpringBoot整体开发的详细过程(待完结)

SpringBoot笔记

文章目录


一、回顾Spring

1.1、概念

spring是一个轻量级的Java开发框架并且开源 为了解决企业级应用开发的复杂性而诞生的,简化开发

  • 如何简化的呢?

为了降低Java开发的复杂性,Spring采用了以下4种关键策略:

  • 所有的类都可以交给Spring来接管创建 Spring中的所有对象都是bean (大杂烩)
  • 通过IOC(控制反转),DI(依赖注入),面向接口编程 将程序实现低耦合
  • 通过AOP编程实现功能的横切 (再不动用原有的代码上动态的增加功能)
  • 通过切面和模版减少样式代码

1.2、IOC、DI的透彻理解

我之前对这一块的了解算是很模糊 不清晰的 因为一开始学Spring的时候本来就没把着重点放在这个上面 但是学完之后 又去看了Springboot 然后又回来看了Spring 发现确实理解加深了一点 下面内容是我在网上看到的一个我自认为很能达到我G点的一个 blog

IoC(控制反转)

  • 先想说说IoC(Inversion of Control,控制反转。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系 这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、微信号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类耦合起来。

  • 那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

DI(依赖注入)

  • IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

理解了IoC和DI的概念后,一切都将变得简单明了,剩下的工作只是在spring的框架中堆积木而已。

1.3、自己的理解

  • IOC 控制反转 我觉得就是在很传统的情况下 我们通常会在自己创建的这个对象里面去显式的得到另一个对象 然后使用另一个对象的所有功能或者一些资源 这样的坏处是什么? 就是我与另一个对象的耦合度极高 因为是紧紧相贴的 我改变了什么还得想着另一个对象会不会跟着被改变一些 这样子明显不符合我们的编程思想 所以在这个基础上 有大佬提出来了控制反转 这么一个概念 这个概念我是这么理解的 就是所有的对象关系 对象创建 对象销毁等等都交给Spring里面的容器去做 一个对象需要什么资源 需要另外一个对象的什么资源 容器会在相应的时机自动给你分配 你只要等着资源到手就行 有了这个第三方的介入 我们所有后备工作都不需要去做 全全交给容器去管理 就好比上面文章所讲:我想泡妹子 我就得去了解她的姓名 qq号 家庭住址等等相关信息 这些事情都是要我亲历亲为的去做这些事情 还不一定能成功!!而IOC思想就是提供了一个婚介所 所有想找女朋友的男孩子 或者想找男朋友的女孩子只要来到这里注册提供相关信息 信息包括:想要找什么类型的 家在哪里 工资多高 或者长得好不好看等等…只要提供给婚介所 婚介所会通过这个请求在你需要的时机提供给你资源 如果你不满意 婚介所也会帮你做好事后处理

  • DI依赖注入的理解上面已经写的很详细了

二、微服务

2.1、概念

微服务其实是一种架构风格 服务微化

每一个应用应该是一组小型服务 每个服务之间使用HTTP互通

  • 描述关系

在这里插入图片描述

2.2、 单体应用时代

在这里插入图片描述

先来打个比方:一开始的淘宝架构也如上所示 都是一个单体应用 所有的功能模块都放在一起 比如说订单模块 商品模块 用户模块等等一系列模块 全组合在一起 这样子确实也挺好 并发量大时 水平拓展同份应用 构建负载均衡 也能承受的住 但是现在问题来了 如果想要新增一个功能 是需要把这个功能在所有服务器上同时上线 这是一个问题 如果某块功能不够完善 需要精益求精 同理越需要改动全身 这种 牵一发而动全身 的服务架构明显不是我们需要的 耦合性太高 所以进而诞生了微服务这种架构风格

2.3、 微服务时代

在这里插入图片描述

微服务就是把每个服务应用拆开来 把应用全部细化 让服务自己组装成一个应用

缺点

  • 运维困难
  • 每个服务部署困难

三、SpringBoot

3.1、概念

  • 言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can “just run”,能迅速的开发web应用,几行代码开发一个http接口。

所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。

是的这就是Java企业级应用->J2EE->spring->springboot的过程。

随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;

  • Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 RedisMongoDBJpaRabbitMQQuartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。

简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。

3.2、优缺点

  • Spring Boot的主要优点:

    • 为所有Spring开发者更快的入门
    • 开箱即用,提供各种默认配置来简化项目配置
    • 内嵌式容器简化Web项目
    • 没有冗余代码生成和XML配置的要求

缺点

  • 最大的缺点就是精通难 因为springboot是基于spring框架再深入或者说再精简 所以说如果没精通spring的api是无法真正掌握springboot的

3.3、为什么要使用springboot?

  • 如上面所示 一是开发一个应用十分简单 每个功能模块 开箱即用 几乎都是0配置 但是最主要的原因是什么?是它推动了微服务的发展 微服务应用构建少不了springboot 我们也了解 微服务这种架构风格每个服务应用构建的话 是个很大的工程量 如果是以前集成SSM框架的技术来构建一个应用 这开发成本是不计其数的 而springboot恰巧集成了SSM框架 轻巧简便 而且集成了大量框架 其中就有微服务相关的 Netflix 它为微服务架构提供了很好的解决方案 这样子一来优势就很明显了 boot为微服务架构提供了很多便利之处:一些优秀的解决方案,应用开发简单,集成了大量的有用框架!!

3.4、第一个SpringBoot程序

1、创建应用

1.1、官网创建

官网:https://spring.io/projects/spring-ws

在这里插入图片描述

在这里插入图片描述

这么一来 大致的项目工程已经建立完毕

1.2、idea工具(推荐使用)

idea集成了springboot工程的创建方式:本质还是从官网上创建的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

IDE都支持使用Spring的项目创建向导快速创建一个Spring Boot项目;

选择我们需要的模块;向导会联网创建Spring Boot项目;

默认生成的Spring Boot项目;

  • 主程序已经生成好了,我们只需要我们自己的逻辑

  • resources文件夹中目录结构

    static:保存所有的静态资源; js css images;

    templates:保存所有的模板页面;(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页
    面);可以使用模板引擎(freemarker、thymeleaf)

    application.properties:Spring Boot应用的配置文件;可以修改一些默认设置;

2、查看结构

  • 项目结构

在这里插入图片描述

3、编写controller

创建完毕后 先写个controller跑一下项目 测试一下项目是否能跑起来

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author chill
 * @date 2021/5/13 22:59
 */
@Controller
public class HelloController {
    //将字符串写在页面上
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}

4、启动项目

启动成功是这样子的

在这里插入图片描述

web环境下 项目是一直在运行的 接下来进行访问

5、访问项目

在这里插入图片描述

通过路径进行访问 项目默认端口是8080 boot初始页面是报错页面 也就是当前所看到的

在这里插入图片描述

这是我们之前写的controller 通过路径成功访问

项目跑成功了 接下来分析一下整个项目结构是什么样子的~~

  • 注意

    • springboot已经集成了大量常用框架…SSM、Secrity等等 内嵌了web容器 自带Tomcat环境 只要开启web支持(导入start启动器 boot则会自动配置好web环境)

3.5、分析SpringBoot应用

1、pom.xml

  • parent
 <!--工程被创建后自带一个父工程  可以点击父工程分析一下-->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 点击去查看到还有一个父工程  点进该父工程查看
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.4.5</version>
  </parent>

分析配置

在这里插入图片描述

在这里插入图片描述

由此我们得知 :

​ 该配置的作用就是整个项目的版本仲裁中心 几乎所有的依赖都不需要导入版本 会被自动仲裁版本

  • gav

创建时的信息 一些jdk版本说明 工程名 还有IP地址

    <!--创建时的信息 一些jdk版本说明	工程名  还有IP地址 gav-->
    <groupId>com.chill</groupId>
    <artifactId>stringboot_study</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>stringboot_study</name>
    <description>chill first springboot project</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

  • dependence
	 <!--
    starter:启动器
    spring-boot-starter:往springboot里添加依赖几乎都是这个开头 后面的后缀见明思意即可
    添加了web支持后 就内嵌了web容器
    -->
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

	<!--springboot自带测试单元-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

项目里需要使用什么功能 就往pom文件中导入相应的启动器starter 导入完毕后所有自动配置会被boot自动装配 只要环境到位 该功能就可以实现 方便了我们maven的书写 以前需要导入大量的jar包才能使用某个功能

  • plugin
<!-- 打包插件-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

自带maven插件 该插件在项目打包生成jar包时起了关键作用 jar包中的启动类标识了哪个应用被开启 可以访问

2、application.yml

文件后缀可以是yml 或者yaml 都会被boot识别 一般文件名默认为 application
如果想要实现环境隔离 区分环境时 则:

  • 环境隔离
    • 准备三个文件 appliction.ymlappliction-dev.ymlappliction-prod.yml
      在这里插入图片描述

    • 三个文件的服务端口都不一致 假设为8001,8002,8003

    • 通过在主文件appliction.yml中设置激活哪个配置文件 程序启动后就会默认执行该文件配置 比如激活的是dev 服务端口就会使用 dev

server:
  port: 8001
spring:
  profiles:
    active: dev
  • 文件说明
    在这里插入图片描述

这个文件是自己需要修改一些自动配置类的默认属性时 可以到这个文件里声明 需要声明一些公共对象也可以在此定义!!!

如果想要在此有提示 的敲代码 必须存在相应的配置类

3、maven

只要安装了maven插件 功能都能使用

boot项目打包特别简单 只需要点击一个按钮即可 傻瓜式打包

在这里插入图片描述

打包成功后会生成

在这里插入图片描述

该jar包包含tomcat 但是不包括整个项目的静态资源 比如说新建的images文件夹 或者一些js html等等

maven工程被编译后都会被解析出一个target目录 该目录包含了大量的工程信息

通过命令java -jar jar包名执行该jar包获得运行结果 结果与在idea跑项目一致

在这里插入图片描述

3.6、自定义Application

官方文档上有讲过 主启动类是可以自定义的 如果我们需要使用一些其他功能 可以自定义 仅作休闲 稍微了解即可

在这里插入图片描述

3.7、配置文件

1、概念

SpringBoot使用一个全局的配置文件 核心配置文件,配置文件名在约定的情况下 名字是固定的; 配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;

  • application.properties

  • application.yml

  • application.yaml

2、两种配置文件的格式

在springboot框架中,resource文件夹里可以存放配置的文件有两种:properties和yml。

1、application.properties的用法

扁平的k/v格式

  server.port=8081 
  server.servlet.context‐path=/chill

2、application.yml的用法

树型结构

 server: 
 	port: 8088 
  	servlet: 
    	context‐path: /chill

两种 前者是,而后者是yml的,建议使用后者,因为它的可读性更强。 可以看到要转换成YML我们只需把properies里按. 去拆分即可。

3、yml基本语法

  • k:(空格)v:表示一对键值对(空格必须有);
  • 以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的
  • 属性和值也是大小写敏感;
  • 如果有特殊字符% & 记得用单引号(‘)包起来

4、配置文件的加载顺序

 <includes> 
	<include>**/application*.yml</include>
	<include>**/application*.yaml</include> 
	<include>**/application*.properties</include> 
</includes>

如果同时存在不同后缀的文件按照这个顺序加载主配置文件;互补配置;

3.8、自动配置原理分析(重点)

  • 分析

所有环境都是主启动类开始被一一启用 那么我们来观察一下主启动类

public class SpringbootOneHelloworldApplication {

    public static void main(String[] args) {
      //这里就是告诉这个boot应用 我们的启动类是哪个类 相当于整个程序的入口 
        SpringApplication.run(SpringbootOneHelloworldApplication.class, args);
    }

}

所以自动配置肯定不在这里 这里是设置整个boot应用的一些基本配置

那么只有一个注解了

@SpringBootApplication

点进该注解进行分析一波 发现这是一个组合注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

慢慢分析:这都是些元注解 学过注解的大概都知道这是些什么意思 所以简单讲讲可以略过

@Target(ElementType.TYPE)	---->该注解能作用在哪些位置   这个只能使用在类上
@Retention(RetentionPolicy.RUNTIME) ---->该类被编译过后 会被记录在类文件中 在运行时也会被虚拟机保留 因此可以通过反射 获取该类的所有信息
@Documented	----> 生成javadoc会被携带上该注解  没什么实际用
@Inherited	---->继承该类 会不会将注解衍生给子类

让我们来看看下一个注解

@SpringBootConfiguration

点进去发现是个组合注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration

这些注解最终的作用就是标注该类为一个配置类 @Configuration 学过spring的同学都知道 该注解标注哪个类 该类就是配置类 且该类会被注册到IOC容器

再下一个注解

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

该注解很正常 就是扫描那些全限定包名底下 带有一些能注册成bean的注解 比如 @Controller@service…等等 excludeFilters就是排除一些过滤器 里面的值表示的意思就是 带有Configuration 或者AutoConfiguration会被扫描到

在这里插入图片描述

@EnableAutoConfiguration

接下来就是最重要的一个注解 该注解分析时需要设置断点进行分析 见名思意 : 启动自动配置

该注解点进去 去掉一些无用的注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage将主启动类所待的包下面的所有类都注册为bean

@Import(AutoConfigurationImportSelector.class) 分析一下 AutoConfigurationImportSelector该类是个什么东西

在这里插入图片描述

该类继承了个 DeferredImportSelector翻译:延期导入选择器 该接口是spring 4.0版本开始出现的,要比它的父接口 ImportSelector晚了几个版本。从文档中我们得知,这是一个变种的ImportSelector接口,它在所有被@Configuration注解修饰的类处理完成后才运行。DeferredImportSelector用在处理@Conditional相关的导入时特别有用

  • ImportSelector:该接口通常被子类实现,用以判断被@Configuration注解修饰的类是否应该被导入;而判断的条件通常是基于注解的一些属性!!

  • DeferredImportSelectorImportSelector的子类 该注解会在所有的@Configuration处理完在导入时才会生效 也就是说该注解有条件所限制 如果该接口的实现类同时实现EnvironmentAware, BeanFactoryAware ,BeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports方法之前先调用上述接口中对应的方法!!!

  • ResourceLoaderAware

在这里插入图片描述

  • BeanFactoryAware

在这里插入图片描述

  • EnvironmentAware
    在这里插入图片描述

  • ResourceLoaderAware

在这里插入图片描述

这些方法都会在调用其selectImports方法时都会被先调用执行

  • selectImports
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
  //
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

Spring的底层注入bean 其实都是依赖selectImports方法

!isEnabled(annotationMetadata) 如果自动配置被启动时 可以重写当前环境属性 如果被重写成功 则返回true 重新配置的新环境就是一个自动配置环境 只有在这个环境下 所有配置才会自动

getAutoConfigurationEntry(annotationMetadata):获取自动配置条目 我们点进去看一下

getAutoConfigurationEntry(annotationMetadata)

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  //老规矩   自动环境到位才能进行如下
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
  //获取注解的所有属性 :  exclude	excludeName
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
  //获取一些符合条件的所有配置  这个好像有用  点进去看一下
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  //删除该配置里面的复制本
		configurations = removeDuplicates(configurations);
  //通过这些属性进行排除  
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
  //通过过滤器再进行筛选一遍  而这里的过滤器选项就是配置类上的 所有带有@Condition 字眼的所有注解都生效  不生效的会被赋为null  过滤掉
		configurations = getConfigurationClassFilter().filter(configurations);
  //一些监听的事件
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

这个方法的作用就是 筛选和过滤出所有符合条件的配置类 所有类 会被 selectImports方法注入到IOC容器中 并进行自动配置

  • getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  //重头戏	这里通过SpringFactoriesLoader加载器来加载一些工厂化的名字 而这个方法需要两个参数 一个是获取这个类是谁	等下点进去源码分析	第二个是获取类加载器 而这个加载器是一开始就被配置过的 
   List<String> configurations = 	SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
  //就是一些简单的断言	不为null就不报错  为null就报错
   Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
         + "are using a custom packaging, make sure that file is correct.");
   return configurations;
}

主要用于获取候选的一些配置 这里已经开始进行第一层筛选 通过类加载器进行筛选

  • loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader())
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
   String factoryTypeName = factoryType.getName();
   return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

用于获取 由SpringFactoriesLoader类加载器加载的类 该类的名称被作为map的key 将该key对应的所有在 spring.factories中的类全部一并返回

  • getSpringFactoriesLoaderFactoryClass()
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
   return EnableAutoConfiguration.class;
}

这个获取所有使用 SpringFactoriesLoader该加载器加载的类

  • getBeanClassLoader()
protected ClassLoader getBeanClassLoader() {   return this.beanClassLoader;}

获取bean 类加载器 用于加载 bean 如果该bean被初始化时已有实例 便不会再创建 否则将创建 SpringFactoriesLoader类的类加载器

loadSpringFactories(classLoader)

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {  //如果该类加载器中存在 则直接从缓存器中取出		Map<String, List<String>> result = cache.get(classLoader);		if (result != null) {			return result;		}		result = new HashMap<>();		try {      // 获取所有包含FACTORIES_RESOURCE_LOCATION该文件 的jar包路径集合  该路径会被排好列一个一个执行			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);      //遍历			while (urls.hasMoreElements()) {				URL url = urls.nextElement();        //将url转化为 UrlResource对象				UrlResource resource = new UrlResource(url);        //加载这个对象的所有属性资源	该属性key ---- 多个value				Properties properties = PropertiesLoaderUtils.loadProperties(resource);				for (Map.Entry<?, ?> entry : properties.entrySet()) {          //获取键名					String factoryTypeName = ((String) entry.getKey()).trim();          //这些都是以 , 隔开的字符串	被组合成一个字符串数组					String[] factoryImplementationNames =							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());/					for (String factoryImplementationName : factoryImplementationNames) {						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())								.add(factoryImplementationName.trim());					}				}			}			// 将所有列表替换为包含唯一元素的不可修改列表			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));			cache.put(classLoader, result);		}		catch (IOException ex) {			throw new IllegalArgumentException("Unable to load factories from location [" +					FACTORIES_RESOURCE_LOCATION + "]", ex);		}		return result;	}

大致的流程已经分析完毕~~ 接下来画个图 然后进行一段总结!!

在这里插入图片描述

总结

只要符合自动配置类上的所有条件注解@Condition… 该自动配置类就会被自动装配 而这些条件满足的情况 大多数都是关于starter 是否被导入 导入了该starter 将条件都满足 则就会自动配置 并且支持自定义配置 而可以自定义配置的内容 属性 都被配置类所声明 yml文件中 可以将默认的属性值更改

3.9、热部署

1、概念

热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用

2、步骤

2.1、导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>
2.2、idea设置

在这里插入图片描述

按键 ctrl+shift+alt +/ 选中 Registry

在这里插入图片描述

找到如下这个选项 给他勾选上

在这里插入图片描述

到此为止 大致的热部署配置已经完成 现在只需要测试一下

每次更新项目代码时 更新完毕后只需等待几秒钟 再次访问即可 大部分情况都不需要重启服务器

4.0、日志框架

1、概念

日志记录在我们编写代码的途中 会显得十分重要 在我们进行调试时 或者服务器发布了之后 用户体验出现了异常需要进行修改代码时 因为发布过后的应用代码量是十分庞大的 如果使用原生的sout 这种调试方式开发成本会非常大 有可能找个输出语句 都需要需要半天 这个时候log 日志就被开发出来了

2、日志框架发展史

  • 小故事

    这是一个故事,故事有一个主角,名字比较特色,很多人叫他ceki,全名是Ceki Gülcü,我们姑且叫他小明吧。

    很久以前,java是没有日志的,调试也只是用system.out.print()去打印日志,这有很大的问题,没有日志怎么查询问题呢,一堆system.out.print有些是部署后想看到的,可是还有很多只是在开发阶段要看到的,如果没有删除就是多余的代码了,怎么办呢?

这时候一个叫小明的主角出现了,他写了一个日志框架,就是鼎鼎大名的log4j,这个实现了日志的展示,日志的级别,报错发送邮件等等功能,这个框架一经出现,便吸引大量粉丝,太方便了!

但也会有各种各样的问题,个性化的,比如我想加个发短信的功能怎么办呢?

这时候小明也看到了这个情况,个人的力量是有限的,小明便开源了log4j代码,让更多的人来积极参与到完善这个框架。

看到这种情况,有个角色出现了,就是阿帕奇,Apache看到这个优秀的框架,行业里面还是空白,便说服了小明加入阿帕奇开源基金会,参与对log4j的维护,小明觉得也不错啊,靠着大boss,很好,便在Apache安心驻扎下来。

Apache收服了小明及他的log4j,觉得很有发展前途,便去游说sun公司,希望sun公司能在java中加入log4j为默认的log日志系统,想法是美好的,结局是残酷的,sun公司看不上Apache这个小公司,他便自己开发一个日志系统,叫做jul,但这个并不是很友好,使用的人也有限吧。

自从log4j之后,各种日志系统也都出现了,各有优缺点,这样出现了一种混乱的现象,就是在整合各系统时,比如spring引用各jar的时候,会发现有不同的日志系统,这怎么办呢,这时候Apache出面了,他做了一个门面的日志系统jcl(之前叫Jakarta Commons Logging,后更名为Commons Logging),这个日志系统不去实现具体的如何打印日志,而是去兼容各种日志系统,统一兼顾,去摆平这种混乱的现象。

小明在Apache干的并不是很开心,特别是看到Apache写的那个jcl太low了,根本不好用,所以他就从Apache出来自己单干,自己写了一个门面,就是大家熟悉的SLF4J,这个出来之后,又拥有了大量粉丝,小明干的不错!

日志系统继续发展,Apache也没闲着,他开发了新一代日志系统,那就是log4j2 ,在log4j的基础上继续发力,比较log4j给了Apache,人家有权利这么做,无可厚非嘛。

而小明看到了log4j2,觉得这个并不好用,还是觉得low,他呢就自己又写了一套日志系统,那就是Logback,这个性能提升了,使用者也是越来越多,很完美!

未来的路还很多,日志系统可能还是继续发展,到底谁与争锋,我们拭目以待。

3、日志框架

  • 传统的日志框架

Java Util Logging(简称JUL) : 这个是jdk 也就是sun公司自己推行的 项目中使用时不需要依赖任何jar包 使用起来十分简单

import java.util.logging.Logger;
public class JulLog {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger("JulLog");
        logger.info("JulLog");
    }
}

Log4J:作者在开发完这个日志框架后实施了开源 受到了广大开源用户的喜好 迅速被人们广泛应用了起来 该日志框架使用起来也非常简单 只需要导入一个maven依赖 使用方式与Java Util Logging极其相似

maven依赖:

 	<dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </dependency>
    </dependencies>
import org.apache.log4j.Logger;
public class Log4J {
	    public static void main(String[] args) {
	        Logger logger = Logger.getLogger("Log4J");
	        logger.info("Log4J");
	    }
	}

注意 :这两个日志导入的包名得好好区分一下 别搞混了

这两个日志已经很久没被更新过了 有点过时了 被淘汰的原因也很清晰明了 就是性能跟不上时代 还有已经满足不了人们现在的需求量

  • 现在流行的日志框架

Log4j2 : 这个是apache基金会仿照着Log4j 进行再次开发 将性能提升了n倍

Logback : 开发Log4j的作者闲不住了 也开发出一个性能性价比很高的日志框架 该框架是 springboot默认的日志是实现类

由于目前微服务架构迅速走红 如果每个模块都是不同的人进行开发 每个人开发习惯都不一样 所以会有可能导致一个现象就是 日志框架使用的不同 可最后需要整合在一块时可能会出现异常 所以诞生了日志门面这种东西

4、日志门面

1、 概念

这不是为了实现日志功能而诞生的一门技术 而是为了整合所有的日志

slf4j : 这是由 Log4j的开发者研究出来的日志门面 该框架是springboot默认的日志门面 !!!

早年,你工作的时候,在日志里使用了log4j框架来输出,于是你代码是这么写的

import org.apache.log4j.Logger; 
\\省略 
  Logger logger = Logger.getLogger(Test.class); 4
  logger.trace("trace");
\\省略

但是,岁月流逝,sun公司对于log4j的出现内心隐隐表示嫉妒。于是在jdk1.4版本后,增加了一个包为java.util.logging,简称 为jul,用以对抗log4j。于是,你的领导要你把日志框架改为jul,这时候你只能一行行的将log4j的api改为jul的api,如下所示

import java.util.logging.Logger; 
 \\省略 
  Logger loggger = Logger.getLogger(Test.class.getName());
  logger.finest("finest");
\\省略

可以看出,api完全是不同的。那有没有办法,将这些api抽象出接口,这样以后调用的时候,就调用这些接口就好了呢? 这个时候jcl(Jakarta Commons Logging)出现了,说jcl可能大家有点陌生,讲commons-logging-xx.jar组件,大家总有印象 吧。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用
程序则可以在运行时搭配自己喜好的日志实践工具。JCL可以实现的集成方案如下图所示

在这里插入图片描述

jcl默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用jul(jdk自带的) 实现,再没有则使用jcl内部提供的 SimpleLog 实现。
于是,你在代码里变成这么写了

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 
\\省略  
  Log log =LogFactory.getLog(Test.class);
	log.trace('trace');
\\省略

至于这个Log具体的实现类,JCL会在ClassLoader中进行查找。这么做,有三个缺点,缺点一是效率较低,二是容易引发混乱, 三是在使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露。

JCL动态查找机制进行日志实例化,执行顺序为:commons­logging.properties­­­­>系统环境变量­­­­­­­>log4j>jul­­­>simplelog­­­­>nooplog
于是log4j的作者觉得jcl不好用,自己又写了一个新的接口api,那么就是slf4j。关于slf4j的集成图如下所示
在这里插入图片描述

理解slf4j日志门面了吗,它跟jcl机制不一样。 它就相当于这个游戏机, 我本身没有游戏, 只提供一个运行游戏的平台(门面) 要运行哪个游戏我不管, 你给我放哪块光盘我就运行哪个游戏。 JCL是自己去找,先找到哪个运行哪个

在这里插入图片描述

Slf4j与其他各种日志组件的桥接说明

jar包名说明
slf4j-log4j12-1.7.13.jarLog4j1.2版本的桥接器,你需要将Log4j.jar加入Classpath。
log4j-slf4j-impl.jarLog4j2版本的桥接器,还需要log4j­api.jar log4j­core.jar
slf4j-jdk14-1.7.13.jarjava.util.logging的桥接器,Jdk原生日志框架。
slf4j-nop-1.7.13.jarNOP桥接器,默默丢弃一切日志。
slf4j-simple-1.7.13.jar个简单实现的桥接器,该实现输出所有事件到System.err. 只有Info以及高于该级别的消息被打印,在小 型应用中它也许是有用的。
slf4j-jcl-1.7.13.jarJakarta Commons Loggin 的桥接器. 这个桥接器将Slf4j所有日志委派给Jcl。
logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar)Slf4j的原生实现,Logback直接实现了Slf4j的接口,因此使用Slf4j与Logback的结合使用也意味更小的内存与计算开销

如图所示,应用调了sl4j-api,即日志门面接口。日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体 的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志 门面接口要想实现与任意日志框架结合可能需要对应的桥接器,上图红框中的组件即是对应的各种桥接器!
我们在代码中需要写日志,变成下面这么写

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
	//省略 
  Logger logger = LoggerFactory.getLogger(Test.class); 
  // 省略
  logger.info("info");

在代码中,并不会出现具体日志框架的api。程序根据classpath中的桥接器类型,和日志框架类型,判断出logger.info应该以什 么框架输出!注意了,如果classpath中不小心引了两个桥接器,那会直接报错的!
因此,在阿里的开发手册上才有这么一条

在这里插入图片描述

ok,至此,基础知识完毕,下面是实战!

4.1、异步框架

1、概念

  • 在某些特殊的业务场景下 我们注册完用户 通常会有这样的需求 将用户注册成功的信息 以短信的形式发送给用户 一是为了验证用户手机号 是否合格 或者 是否能正确接收信息 二是将用户体验提升 还有可能会有积分加持 那么情况就来了!!!如果程序执行期间 积分加持这块业务功能出错了 那么在 串行执行 这种模式中 可能造成我们用户注册失败 但是因为一个附属的功能 导致流失掉一个用户 这代价明显是很大的 作为解决方案 所以异步框架的好处就显而易见了!!!

2、大致业务需求

在这里插入图片描述

  • 最主要需要实现的是 注册成功 其他的附属功能只是将用户体验提升到最大

3、区别

在这里插入图片描述

  • 在双方执行时 并行执行是让用户更好的体验 用户注册 功能
  • 而在真是场景中 一定要注意并不是什么时候都能用 异步来处理 千万不要滥用 一定根据合适的业务场景来使用 就比如一些附属的功能 都能通过 异步来完成

4、实现异步

4.1、启动类添加支持
package com.chill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringbootCliApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootCliApplication.class, args);
    }
}
4.2、编写Controller层

package com.chill.controller;

import com.chill.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-26
 * @Version : v1.0
 */
@RestController
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 采用异步执行
     *
     * @return
     */
    @RequestMapping("/saveUser")
    public String saveUser() {
        //串行执行
        /*log.info("用户注册!!!");
        log.info("发送短信!!!");
        log.info("增加积分!!!");*/


        //改进后


        //并行执行
        log.info("用户注册!!!");
        userService.sendMessage();
        userService.addPoint();
        return "success";
    }
}
  • 这里都是模拟真实业务场景 所以写的比较简洁
4.3、编写业务层
package com.chill.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-26
 * @Version : v1.0
 */
@Service
@Slf4j
public class UserService {


    @Async
    public void sendMessage() {
        try {
            Thread.sleep(5000);
            log.info("发送短信!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Async
    public void addPoint() {
        try {
            Thread.sleep(5000);
            log.info("增加积分!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
4.4、运行结果
  • 通过ip地址:端口 localhost:8002访问程序
    在这里插入图片描述
  • 访问成功 而且我在业务层加了 线程延时的 但是在我访问时 结果响应的很快 前后不超过1-2s
  • console
    在这里插入图片描述
4.5、小结

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

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

5、异步线程池的优化

5.1、分析

通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。
在这里插入图片描述

5.2、问题
  • 其实基于上面的开发 如果在小并发情况下去使用 是没有什么问题的 而我们的服务器 肯定不是基于小规模去访问的
  • @Async的默认情况下使用的是 SimpleAsyncTaskExecutor 线程池 它并不是一个实际意义上的线程池 事实上 它通常会给每个异步方法 新开一个线程 而这个线程被使用过后不会被回收
  • 如果接下来会有新的异步方法 它会新开线程去执行 并不会重用线程 这样子并发量一大 服务器根本承受不住的
5.2、线程池的简单理解
  • 线程池会有默认的一些核心线程 用来执行一些基本的方法
  • 线程池一般会设置 线程数的上限 如果线程数达到上限 会将方法先阻塞在线程池外面 等待线程池里面的线程 一旦有空闲线程就会去执行这些被阻塞的方法
  • 线程池会有一个类似于 哨兵 一样的东西(可以简单理解为哨兵) 它负责监督线程池里面的状况 如果线程池里面空余线程剩余过多 且在规定时间内(一般是3-5分钟)没有使用 则会被销毁
5.3、@Async 注解支持的线程机制
  • SimpleAsyncTaskExecutor:简单的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  • SyncTaskExecutor:这个类没实现异步调用,只是一个同步操作,只适合用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用.。
  • ThreadPoolTaskScheduler:可以和cron表达式使用。
  • ThreadPoolTaskExecutor最常用,推荐,其本质就是:java.util.concurrent.ThreadPoolExecutor的包装

如果想要更改 @Async 默认线程池 只需要在此基础上进行覆盖就好了~~

package com.chill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @Author chill
 */
@Configuration
public class SyncThreadPoolConfiguration {

    /**
     * 把springboot中的默认的异步线程线程池给覆盖掉。用ThreadPoolTaskExecutor来进行处理
     **/

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor getThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        // 1: 创建核心线程数 cpu核数 -- 50  
        threadPoolTaskExecutor.setCorePoolSize(10);
        // 2:线程池维护线程的最大数量,只有在缓存队列满了之后才会申请超过核心线程数的线程
        threadPoolTaskExecutor.setMaxPoolSize(100);
        // 3:缓存队列 可以写大一点无非就浪费一点内存空间 也就是当前可以执行最大的线程数量
        threadPoolTaskExecutor.setQueueCapacity(200);
        // 4:线程的空闲时间,当超过了核心线程数之外的线程在达到指定的空闲时间会被销毁 200s
        threadPoolTaskExecutor.setKeepAliveSeconds(200);
        // 5:异步方法内部线的名称 自定义 chill-thread-
        threadPoolTaskExecutor.setThreadNamePrefix("chill-thread-");
        // 6:缓存队列的策略
        /* 当线程的任务缓存队列已满并且线程池中的线程数量已经达到了最大连接数,如果还有任务来就会采取拒绝策略,
         * 通常有四种策略:
         *ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常:RejectedExcutionException异常
         *ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
         *ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         *ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用execute()方法,直到成功。
         *ThreadPoolExecutor. 扩展 重试3次,如果3次都不充公在移除。
         *jmeter 压力测试 1s=500
         * */
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}
5.4、小结
  • 异步不要泛滥使用 一般使用于的场景:
    • 一些核心功能的附属业务
    • 需要开辟多个线程执行任务

异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)

四、Web开发

4.1、说明

  • 在进行web开发时 我们通常会采用 get/post 方式(最常用 当然还有RestFul风格的DELETE PUT等等)进行访问controller接口

  • 那么我们在实际开发中到底如何使用 如何去衡量呢?

  • 这就需要我们得搞清GetPost请求的区别了

  • 区别

    • 后端取值方式不同
      • get 会将请求参数包含在 url 中 可以通过 @RequestParam() 将参数值取出 而post 请求得数据都存在于 请求体中 也就是我们的RequestBody 所以我们需要添加 @RequestBody 来将数据取出
    • get和post的缓存机制
      • get方式如果使用于请求后端的静态资源 会将 静态资源 缓存一份在浏览器中 所以这就可以说明 为什么在请求后端的静态资源时一般使用的是 get 因为第二次进行请求时 直接从缓存中取出 快捷 效率高 所以我们思考一下 为什么在更新静态资源过后 有时需要清理浏览器的缓存?
      • post 请求每次都需要去后端获取 每次的时间几乎都一致!! 网上有人证明过啦 可以去翻阅一下
    • get比post请求更快
      • 首先post 请求会将大量的数据包含在请求头内 而这些请求头需要先通过服务端的确认 这个确认也很直白 就是 requestBody的接收结果 如果成功才将数据一一发送过来 这个过程是有一点耗时消耗性能
        • post 请求的过程:
        1. 浏览器请求tcp连接(第一次握手)
        2. 服务器答应进行tcp连接(第二次握手)
        3. 浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
        4. 服务器返回100 Continue响应
        5. 浏览器发送数据
        6. 服务器返回200 OK响应
        • get 请求的过程:
        1. 浏览器请求tcp连接(第一次握手)
        2. 服务器答应进行tcp连接(第二次握手)
        3. 浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
        4. 服务器返回200 OK响应

也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。

4.2、统一结果返回 R类

1、概念

  • 在目前这个时代 项目基本都是前后端分离的 所以在我们前后端进行数据交互时 应该要存在一个标准 使得我们开发更为简便 所以提出了 R类

2、R类结构

  • Result
  • 基于springMvc和MybatisPlus里的源码总结了一部分经验 事实上我们为什么不用已经定义好的呢?
  • 答案是:无法满足我们的需求 大多数消息设置 或者 状态码设置都需要我们自定义
package com.chill.common.base;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;

/**
 * 统一返回结果类
 */
@Data
@ToString
public class Result implements Serializable {
    private static final long serialVersionUID = 986823857621547280L;
    private Boolean success; //是否成功
    private Integer code;   //状态码
    private String message; //具体消息  成功失败与否的消息
    private Object data;    //具体数据
    private Result() {
    }

    /**
     * 请求成功 没有数据 只返回状态码
     * @return
     */
    public static Result ok() {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }

    /**
     * 请求成功 有数据
     * @param data
     * @return
     */
    public static Result ok(Object data) {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        r.setData(data);
        return r;
    }
    /**
     * 错误请求 返回错误消息
     * @return
     */
    public static Result error() {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
        return r;
    }

    /**
     * 最终版 参数设置为枚举 设置结果集的内容
     * @param resultCodeEnum
     * @return
     */
    public static Result setResult(ResultCodeEnum resultCodeEnum) {
        Result r = new Result();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }
    public Result success(Boolean success) {
        this.setSuccess(success);
        return this;
    }
    public Result message(String message) {
        this.setMessage(message);
        return this;
    }
    public Result code(Integer code) {
        this.setCode(code);
        return this;
    }
    public Result data(Object o) {
        this.setData(o);
        return this;
    }
}
  • ResultCodeEnum
  • 返回结果的枚举 包括 状态 消息等等
package com.chill.common.base;

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

至此 我们在返回结果时只需要返回R类便行

  • 示例
@GetMapping("/success")
    public Result success() {
        return Result.ok();
    }

但是这样真的没有问题吗?
思考:

  • 如果我们是一个比较大的开发团队 不可能所有人都会听从安排 以R类去返回结果 事实上 大多数开发人都有自己的代码风格 所以我们需要另辟蹊径 再想个法子 可以自己还是安装自己的风格去返回数据 但是都会被包裹一层 R类出去 这样子是不是就能解决我们的问题呢

3、解决方案

  • springmvc给我们提供了这样的支持 只需要实现 ResponseBodyAdvice接口 重写方法 便能满足我们的需求

    • ResponseBodyAdvice
      • 它是属于springAOP机制的一种实现 属于后置通知 在我们mvc将结果返回给前端时 会被它拦截下来进行处理
      • 通常我们会对结果进行增强处理 比如 加密、签名、字符串特殊处理等等
实现
package com.chill.handle;

import com.chill.common.base.Error;
import com.chill.common.base.Result;
import com.chill.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;

/**
 * ResponseBodyAdvice: 所有返回值都会经过它检查 它会在mvc层返回数据给前端之前执行 一般对返回值进行加密 签名等等
 * 解决的问题:并不是非要所有返回类型都要是 R 类     局限性没那么高
 */
@ControllerAdvice(basePackages = "com.chill")
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;
    }

    /**
     * 这个方法是否被执行 由上面的supports()的返回值决定
     * @param o controller方法返回的结果会被映射到该对象中
     * @param methodParameter   方法的参数 也就是当前这个controller的方法
     * @param mediaType
     * @param aClass    除开字符串   一般的类型都会被HttpMessageConverter处理过后再交给前端   字符串需要特殊处理
     * @param serverHttpRequest 请求头
     * @param serverHttpResponse 响应头
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //判断一下 该方法是否异常  因为如果出现异常的话 这个对象会被包装成Error
        if (o instanceof Error) {
            Error error = (Error) o;
            //将error对象再裹一层 --->Result
            return Result.error().code(error.getStatus()).message(error.getMessage());
        } else if (o instanceof String) {
            //因为如果返回是string的话默认会调用string的处理器会直接返回,所以要进行处理
            return JSONUtil.toJsonStr(Result.ok(o));
        }
        //如果都没问题 则直接包装成 Result
        return Result.ok(o);
    }
}
  • 说明

    • 在执行顺序中 如果controller方法出现异常 会先被捕捉到全局异常中 然后再进入这个 后置通知 ResponseBodyAdvice
    • 如果controller方法 返回值是 String的话 要进行 特殊处理 使用市面上的一些Json工具包 将字符串转为Json即可 我使用的是 hutool-all
    <dependency>
         <groupId>cn.hutool</groupId>
         <artifactId>hutool-all</artifactId>
         <version>5.7.2</version>
    </dependency>
    
    • 需要加上@ControllerAdvice(basePackages = “com.chill”) 因为如果不加的话,可能给整个系统的产生冲突影响比如:如果你使用了swagger时会出现空白异常
测试
  • 如果不处理 String 的后果
    在这里插入图片描述

    • 原因是什么呢?
      • 因为在Result.ok(o) 这句代码时 此时 o 已经确定好是String 它会调用 StringHttpMessageConverter对它进行转换 它是 springmvc 将字符串传入前端时默认的转换器 作用于转换字符串为Json 然而这里已经被包装成Result对象了 而StringHttpMessageConverter 只能转换 String 所以便报了错
  • 正确结果

    • controller
    @GetMapping("/ok")
        public String ok() {
            return "success";
        }
    
    • 响应结果
      在这里插入图片描述

4.3、全局异常处理

1、概念

  • 在我们开发中 不可能会使用大量的 try/catch 来处理异常 这样一来显得代码十分臃肿 二来处理程序十分繁琐 所以通常我们会采用 springmvc 内部提供的的统一异常处理 它是为了解决一系列错误的方案

2、实现

2.1、编写Error和ExceptionCodeEnum
  • Error
  • Result类相似 主要不想影响Result 所以单独抽出来作为一个异常结果返回类
package com.chill.common.base;

import lombok.*;


/**
 * 统一返回异常类
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Error {
    // 异常的状态码,从枚举中获得
    private Integer status;
    // 异常的消息,写用户看得懂的异常,从枚举中得到
    private String message;
    // 异常的名字
    private String exception;

    /**
     * 对异常处理进行统一封装
     *
     * @param exceptionCodeEnum : 异常状态枚举
     * @param throwable         所有异常或者错误的顶级父类
     * @param message           错误信息
     * @return
     */
    public static Error fail(ExceptionCodeEnum exceptionCodeEnum, Throwable throwable, String message) {
        Error error = Error.fail(exceptionCodeEnum, throwable);
        error.setMessage(message);
        return error;
    }

    /**
     * 对异常枚举进行封装
     *
     * @param resultCodeEnum
     * @param throwable
     * @return
     */
    public static Error fail(ExceptionCodeEnum resultCodeEnum, Throwable throwable) {
        Error error = new Error();
        error.setMessage(resultCodeEnum.getMessage());
        error.setStatus(resultCodeEnum.getCode());
        error.setException(throwable.getClass().getName());
        return error;
    }
}
  • ExceptionCodeEnum
  • 异常错误信息的枚举 包括状态码 还有异常信息异常类
package com.chill.common.base;

import lombok.Getter;


/**
 * 异常状态码枚举
 */
@Getter
public enum ExceptionCodeEnum {
    UNKNOWN_REASON(false, 20001, "未知错误"),
    SERVER_ERROR(false, 500, "服务器忙,请稍后在试"),
    ORDER_CREATE_FAIL(false, 601, "订单下单失败");
    private Boolean success;
    private Integer code;
    private String message;

    private ExceptionCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

将常量类升级使用枚举?

  • 如果有常量类里面 成批 成对的一些常量 可以考虑使用枚举 枚举具有面向对象的特征 代码也十分清晰明了 不臃肿
2.2、编写配置类
  • 新建一个包 handle 创建一个类GlobalExceptionHandler
package com.chill.handle;

import com.chill.common.base.Error;
import com.chill.common.base.ExceptionCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 *
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-28
 * @Version : v1.0
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


    /**
     * @param throwable
     * @return
     * @ExceptionHandler : 用于捕捉在程序中出现异常的代码
     */
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) //指定该方法处理哪个状态码的异常  比如INTERNAL_SERVER_ERROR(内部服务器错误) 就是 500
    @ExceptionHandler(Throwable.class)
    public Error processException(Throwable throwable) {
        log.error("{}", throwable);  //记录错误信息
        return Error.fail(ExceptionCodeEnum.SERVER_ERROR, throwable);   //出现错误后返回自己的错误信息
    }
}

注解说明:

  • RestControllerAdviceControllerAdvice 的区别
    • RestControllerAdvice注解的类里面的方法在捕捉异常后是不能进行页面跳转的 而 ControllerAdvice是可以的 也就是说和Controller层的那两个注解意思有一点类似 RestControllerController
  • ResponseStatus
    • 指定该方法处理哪个状态码的异常 比如 INTERNAL_SERVER_ERROR(内部服务器错误) 就是 500
  • ExceptionHandler
    • 被该注解标识的方法 在捕捉到异常后 会进入该方法
2.3、测试
  • controller
@GetMapping("/error")
    public String error() {
        int num=1/0;
        return null;
    }
  • 发送请求
    在这里插入图片描述

报完错以后进入 我们的全局异常里
在这里插入图片描述

  • 响应结果
    在这里插入图片描述

符合我们的正确结果 但是问题来了 思考一下:如果所有controller方法出现了异常 那么都走全局异常 然后报 500 错误 这样子异常错误清晰吗?根据不清晰 对于我们开发这是很难的事情!!所以我们需要优化它。

3、自定义异常

3.1、概念
  • 为了我们能够更快的定位异常信息 和 记录精确的日志 我们需要对全局异常类进行优化 我们可以在订单下单失败时 返回给前端明确的信息 : 订单下单失败 这样子一来 我们能够快速的定位错误和排查
3.2、实现
1、编写自定义异常类
  • 这是与订单相关的异常类 但凡在订单生成时出错 都会报出该错
  • 一般的异常错误几乎都是运行时异常 所以我们只需要继承RuntimeException就行了
package com.chill.custom;

import com.chill.common.base.ExceptionCodeEnum;
import lombok.Data;

/**
 * 自定义异常类
 */
@Data
public class OrderException extends RuntimeException {
    private Integer code;   //订单错误状态码
    private String message; //订单错误信息

    //ExceptionCodeEnum 包含了所有具体异常的枚举
    public OrderException(ExceptionCodeEnum exceptionCodeEnum) {
        this.code = exceptionCodeEnum.getCode();
        this.message = exceptionCodeEnum.getMessage();
    }
}
2、编写异常枚举类
  • 它包含了所有异常状态码 信息等等
  • 枚举 方便扩展 对外修改关闭 对内修改打开
package com.chill.common.base;

import lombok.Getter;


/**
 * 异常状态码枚举
 */
@Getter
public enum ExceptionCodeEnum {
    UNKNOWN_REASON(false, 20001, "未知错误"),
    SERVER_ERROR(false, 500, "服务器忙,请稍后在试"),
    ORDER_CREATE_FAIL(false, 601, "订单下单失败");
    private Boolean success;
    private Integer code;
    private String message;

    private ExceptionCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}
3、编写全局异常类
  • 新增一个异常方法 该异常用来专门处理订单
  • 在捕捉异常时 springmvc的机制是 默认从小到大 也就是说 Throwable是顶级父类 在子类解决不了问题时 才会把问题丢给这个父类 有一点像JS的冒泡 或者JVM的双亲委派机制

 /**
     * 捕捉自定义异常
     * @param orderException    精确到 订单异常
     * @return
     */
    @ExceptionHandler(OrderException.class)
    public Error processOrderException(OrderException orderException) {
        log.error("{}", orderException.getMessage());
        return Error.builder().
                status(orderException.getCode()).
                message(orderException.getMessage()).
                exception(orderException.getClass().getName()).
                build();
    }
4、测试
	 //测试自定义异常
    @GetMapping("/order")
    public User order() {
        throw new OrderException(ExceptionCodeEnum.ORDER_CREATE_FAIL);
    }
5、结果

通过swagger页面进行访问

  • console
    在这里插入图片描述
  • swagger
    在这里插入图片描述

结果正是我们想要的

4、小结
  • 在我们需求增加时 我们想要获取异常的精确信息 和 快速定位异常 我们需要对全局异常进行优化 这个优化springmvc 也提供了支持 就是自定义异常
  • 通过自定义异常 我们可以将精确异常信息记录到日志中 从而快速排查bug
  • 我们应该对于一些常见的异常错误 我们可以采用自定义异常 专门捕捉

4.4、校验器(Validator)

1、概念

  • 在日常开发中 我们需要对数据格式进行严格的排查后 才能写入数据库 然而在如今前后端分离的情况下 在前端表单校验数据过后 其实后端也需要保证万无一失 所以也要再校验一遍 校验的参数一般会有pojo普通的参数等等 那么对于这些我们如何去校验呢 ?
  • 因为在日常的开发中,服务端对象的校验是非常重要的一个环节,比如:注册的时候:校验用户名,密码,身份证,邮箱等信息是否为空,以及格式是否正确,但是这种在日常的开发中进行校验太繁琐了,代码繁琐而且很多。
  • Validator框架应运而生,它的出现就是为了解决开发人员在开发的时候减少代码的,提升开发效率。它专门用来做接口的参数校验,比如:密码长度、是否为空等等。

spring的 validator校验框架遵守的是JSR-303的验证规范(参数校验规范),JSR全称:Java Specification Requests缩写。

在默认情况下:SpringBoot会引入Hibernate Validation机制来支持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模块中添加了自动校验。并将校验信息封装到特定的类中。

在这里插入图片描述

2、常用的校验注解

  • 2/3 的注解都在这了 意思也写的很明白
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=)  被注释的元素必须在合适的范围内

3、整合validator

3.1、核心步骤概述

在实际开发中,只需要三个步骤:

  • 在需要校验的pojo,vo等等中的属性上增加对应注解,比如@NotBlank

  • controller方法参数中的 pojo,vo中加 @Validated 的注解即可。(普通参数有另外的处理方法)

  • 使用全局统一异常处理捕获的验证失败的提示信息

3.2、实现
1、pom.xml添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、新建用户实体
  • 结合校验注解实现该实体
  • 在实际开发中 有些需要使用自定义校验器来完成特殊的需求
package com.chill.vo;

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

/**
 * @author chill
 * @Description: vo类
 * @date 2021/6/29 14:29
 */
@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;
}
3、编写controller
	//测试校验器
    @PostMapping("/validate")
    public UserVo validate(@RequestBody @Validated UserVo user) {
        log.info("{}",user);
        return user;
    }
4、测试
  • swagger工具进行调试 或者postman 等等
    在这里插入图片描述
5、结果
  • console
    在这里插入图片描述

  • swagger
    在这里插入图片描述

6、问题
  • 确实是报错了 但是问题是这个错误信息都打印在控制台 而且错误信息十分复杂 并不精确 所以我们得在全局异常类哪里动点手脚 新增一个捕捉异常方法
3.3、优化异常信息
1、分析
  • 如果你校验失败,springmvc的validator内部会以异常的方式进行返回。报错异常:MethodArgumentNotValidException 而这个异常里面,包含所有的校验的提示信息。

  • 那么MethodArgumentNotValidException 我们可以通过异常的api 取出重要的信息

2、实现
  • 对于异常 我们可以取出异常里的一些重要信息 而这些重要信息我们需要记录日志 和 返回给前端
	/**
     * 对验证的统一异常进行统一处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Error processValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
        //获取与字段相关的所有错误
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        //该方法将fieldErrors里面的信息 简化  取出了最重要的一部分  key:字段名  value:错误信息
        List<Map<String, String>> mapList = processFieldErrors(fieldErrors);
        Error error = Error.fail(ExceptionCodeEnum.PARAMS_VALIDATION_ERROR, e, JSONUtil.toJsonStr(mapList));
        return error;
    }
	 /**
     * 将错误验证信息取出重要部分进行返回
     * @param fieldErrorList
     * @return
     */
    private List<Map<String, String>> processFieldErrors(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;
    }
3、测试
  • swagger
    在这里插入图片描述
4、debug分析
  • 通过异常的api取出所有异常字段对象 它是一个集合
    在这里插入图片描述

  • 将集合中所有的异常字段对象 最重要的信息取出 并存入map 使用json工具转化为字符串传入前端
    在这里插入图片描述

3.4、小结
  • 这是将全局异常、参数校验和统计返回类 做了一个总结性的使用 可以看出我们如果想要精确化的数据 或者 信息 都需要我们自己自定义一些东西来实现需求
  • 上面有个问题 发现了吗?
    • 参数都是实体类或者都是一个对象 那么问题来了 如果是一个普通参数 我们可以使用 @Validated 来验证吗? 并不能

4、验证普通参数(使用Assert)

4.1、概念
  • Assert是 断言 的意思
  • Web 应用在接受表单提交的数据后都需要对其进行合法性检查,如果表单数据不合法,请求将被驳回。类似的,当我们在编写类的方法时,也常常需要对方法入参进行合法性检查,如果入参不符合要求,方法将通过抛出异常的方式拒绝后续处理。
  • 一般使用:通过断言过后 如果参数不符合要求 则直接报异常 IllegalArgumentException
  • 所以我们需要针对IllegalArgumentException 也得写一个自定义异常捕捉方法
4.2、实现
1、编写异常捕捉方法
  • 非法参数异常 全部会被该方法捕捉到
/**
     * 普通参数校验统一处理
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Error handlerIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
        Error error = Error.builder()
                .status(4000)
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("请求的地址是:{},IllegalArgumentException出现异常:{}", request.getRequestURL(), e);
        return error;
    }
2、编写controller
//测试校验器
    @PostMapping("/validate2")
    public String validate2(@RequestBody String name) {
        Assert.isNull(name,"用户名不能为空");
        log.info("{}",name);
        return name;
    }
3、测试
  • swagger发送请求
    在这里插入图片描述
4、结果
  • console
    在这里插入图片描述
  • swagger
    在这里插入图片描述
    就这么简单 ~~

5、自定义校验器

5.1、概念
  • 很多时候 由Validator 框架给我们提供的 一些验证注解 并不够用 我们通常还有一些其他的需求 所以我们就需要自定义校验器来实现我们的需求
5.2、实现
1、定义验证异常注解
  • 随便copy一个验证注解 稍微修改一下就好了 比如说:@Email
  • 核心在 @Constraint(validatedBy = PhoneValidator.class) 需要给指定一个约束 该约束由自己定义 PhoneValidator
package com.chill.custom.validate;

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、定义自定义校验器
  • 需要实现ConstraintValidator 接口 该接口的作用就是 (源码得知:) 给 T这个对象添加 A 的约束
  • 也就是说根据具体传入的对象 T : String A@Phone 注解 给该String增加一个约束 该约束就是只能为@Phone里面定义的规则 而Phone依赖于PhoneValidatorPhoneValidator 里有一个isValid方法 它就定义了具体规则 如果规则验证通过则返回true 验证失败则返回false
package com.chill.custom.validate;

import com.chill.tools.ValidateUtil;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description: 给该 Phone注解对象定义一个约束  该约束的具体实现都在isValid 里  是否验证通过
 * @author: chill
 * @time: 2021/6/29 20:57
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
        // 1: 如果用户没输入直接返回不校验,因为空的判断应该交给@NotNull去做就行了
        if (StringUtils.isEmpty(phone)) {
            return true;
        }
        return ValidateUtil.validateMobile(phone);
    }

	 //可以在执行isValid之前 先初始化该注解  
    @Override
    public void initialize(Phone constraintAnnotation) {
    }
}
3、测试
  • 在需要加的属性上 添加即可
  • 比较简单 就不演示了

6、验证工具包

  • 在多数情况下 我们需要自己定义一些工具包 来帮助我们快速开发
package com.chill.tools;

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);
    }
}
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值