赞
踩
spring是一个轻量级的Java开发框架并且开源 为了解决企业级应用开发的复杂性而诞生的,简化开发
为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
我之前对这一块的了解算是很模糊 不清晰的 因为一开始学Spring的时候本来就没把着重点放在这个上面 但是学完之后 又去看了Springboot 然后又回来看了Spring 发现确实理解加深了一点 下面内容是我在网上看到的一个我自认为很能达到我G点的一个 blog
先想说说IoC(Inversion of Control,控制反转
)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系
这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、微信号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类耦合起来。
那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
理解了IoC和DI的概念后,一切都将变得简单明了,剩下的工作只是在spring的框架中堆积木而已。
IOC 控制反转 我觉得就是在很传统的情况下 我们通常会在自己创建的这个对象里面去显式的得到另一个对象 然后使用另一个对象的所有功能或者一些资源 这样的坏处是什么? 就是我与另一个对象的耦合度极高 因为是紧紧相贴的 我改变了什么还得想着另一个对象会不会跟着被改变一些 这样子明显不符合我们的编程思想 所以在这个基础上 有大佬提出来了控制反转
这么一个概念 这个概念我是这么理解的 就是所有的对象关系 对象创建 对象销毁等等都交给Spring里面的容器去做 一个对象需要什么资源 需要另外一个对象的什么资源 容器会在相应的时机自动给你分配 你只要等着资源到手就行 有了这个第三方的介入 我们所有后备工作都不需要去做 全全交给容器去管理 就好比上面文章所讲:我想泡妹子 我就得去了解她的姓名 qq号 家庭住址等等相关信息 这些事情都是要我亲历亲为的去做这些事情 还不一定能成功!!而IOC思想
就是提供了一个婚介所 所有想找女朋友的男孩子 或者想找男朋友的女孩子只要来到这里注册提供相关信息 信息包括:想要找什么类型的 家在哪里 工资多高 或者长得好不好看等等…只要提供给婚介所 婚介所会通过这个请求在你需要的时机提供给你资源 如果你不满意 婚介所也会帮你做好事后处理
DI依赖注入的理解上面已经写的很详细了
微服务其实是一种架构风格
服务微化
每一个应用应该是一组小型服务 每个服务之间使用HTTP互通
先来打个比方:一开始的淘宝架构也如上所示 都是一个单体应用 所有的功能模块都放在一起 比如说订单模块 商品模块 用户模块等等一系列模块 全组合在一起 这样子确实也挺好 并发量大时 水平拓展同份应用 构建负载均衡 也能承受的住 但是现在问题来了 如果想要新增一个功能 是需要把这个功能在所有服务器上同时上线 这是一个问题 如果某块功能不够完善 需要精益求精 同理越需要改动全身 这种 牵一发而动全身 的服务架构明显不是我们需要的 耦合性太高 所以进而诞生了微服务这种架构风格
微服务就是把每个服务应用拆开来 把应用全部细化 让服务自己组装成一个应用
缺点:
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”
,进而衍生出一些一站式的解决方案。
是的这就是Java企业级应用->J2EE->spring->springboot的过程。
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
Redis
、MongoDB
、Jpa
、RabbitMQ
、Quartz
等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。
Spring Boot的主要优点:
缺点:
Netflix
它为微服务架构提供了很好的解决方案 这样子一来优势就很明显了 boot为微服务架构提供了很多便利之处:一些优秀的解决方案,应用开发简单,集成了大量的有用框架!!官网:https://spring.io/projects/spring-ws
这么一来 大致的项目工程已经建立完毕
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应用的配置文件;可以修改一些默认设置;
创建完毕后 先写个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"; } }
启动成功是这样子的
web环境下 项目是一直在运行的 接下来进行访问
通过路径进行访问 项目默认端口是8080 boot初始页面是报错页面 也就是当前所看到的
这是我们之前写的controller 通过路径成功访问
项目跑成功了 接下来分析一下整个项目结构是什么样子的~~
注意
<!--工程被创建后自带一个父工程 可以点击父工程分析一下-->
<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>
分析配置
由此我们得知 :
该配置的作用就是整个项目的版本仲裁中心 几乎所有的依赖都不需要导入版本 会被自动仲裁版本
创建时的信息 一些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>
<!-- 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包才能使用某个功能
<!-- 打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
自带maven插件 该插件在项目打包生成jar包时起了关键作用 jar包中的启动类标识了哪个应用被开启 可以访问
文件后缀可以是yml 或者yaml 都会被boot识别 一般文件名默认为 application
如果想要实现环境隔离 区分环境时 则:
准备三个文件 appliction.yml
,appliction-dev.yml
,appliction-prod.yml
三个文件的服务端口都不一致 假设为8001,8002,8003
通过在主文件appliction.yml
中设置激活哪个配置文件 程序启动后就会默认执行该文件配置 比如激活的是dev
服务端口就会使用 dev
的
server:
port: 8001
spring:
profiles:
active: dev
这个文件是自己需要修改一些自动配置类的默认属性
时 可以到这个文件里声明 需要声明一些公共对象也可以在此定义!!!
如果想要在此有提示 的敲代码 必须存在相应的配置类
只要安装了maven插件 功能都能使用
boot项目打包特别简单 只需要点击一个按钮即可 傻瓜式打包
打包成功后会生成
该jar包包含tomcat 但是不包括整个项目的静态资源 比如说新建的images文件夹 或者一些js html等等
maven工程被编译后都会被解析出一个target目录 该目录包含了大量的工程信息
通过命令java -jar jar包名
执行该jar包获得运行结果 结果与在idea跑项目一致
官方文档上有讲过 主启动类是可以自定义的 如果我们需要使用一些其他功能 可以自定义 仅作休闲 稍微了解即可
SpringBoot使用一个全局的配置文件 核心配置文件,配置文件名在约定的情况下 名字是固定的; 配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;
application.properties
application.yml
application.yaml
在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里按. 去拆分即可。
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
如果同时存在不同后缀的文件按照这个顺序加载主配置文件;互补配置;
所有环境都是主启动类开始被一一启用 那么我们来观察一下主启动类
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
注解修饰的类是否应该被导入;而判断的条件通常是基于注解的一些属性!!
DeferredImportSelector
:ImportSelector
的子类 该注解会在所有的@Configuration处理完在导入时才会生效 也就是说该注解有条件所限制 如果该接口的实现类同时实现EnvironmentAware, BeanFactoryAware ,BeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports方法之前先调用上述接口中对应的方法!!!
ResourceLoaderAware
EnvironmentAware
ResourceLoaderAware
这些方法都会在调用其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容器中 并进行自动配置
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;
}
主要用于获取候选的一些配置 这里已经开始进行第一层筛选 通过类加载器进行筛选
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中的类全部一并返回
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
这个获取所有使用 SpringFactoriesLoader
该加载器加载的类
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文件中 可以将默认的属性值更改
热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
按键 ctrl+shift+alt +/ 选中 Registry
找到如下这个选项 给他勾选上
到此为止 大致的热部署配置已经完成 现在只需要测试一下
每次更新项目代码时 更新完毕后只需等待几秒钟 再次访问即可 大部分情况都不需要重启服务器
日志记录在我们编写代码的途中 会显得十分重要 在我们进行调试时 或者服务器发布了之后 用户体验出现了异常需要进行修改代码时 因为发布过后的应用代码量是十分庞大的 如果使用原生的sout
这种调试方式开发成本会非常大 有可能找个输出语句 都需要需要半天 这个时候log
日志就被开发出来了
小故事
这是一个故事,故事有一个主角,名字比较特色,很多人叫他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,这个性能提升了,使用者也是越来越多,很完美!
未来的路还很多,日志系统可能还是继续发展,到底谁与争锋,我们拭目以待。
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默认的日志是实现类
由于目前微服务
架构迅速走红 如果每个模块都是不同的人进行开发 每个人开发习惯都不一样 所以会有可能导致一个现象就是 日志框架使用的不同 可最后需要整合在一块时可能会出现异常 所以诞生了日志门面
这种东西
这不是为了实现日志功能而诞生的一门技术 而是为了整合所有的日志
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动态查找机制进行日志实例化,执行顺序为:commonslogging.properties
>系统环境变量
>log4j
>jul
>simplelog
>nooplog
于是log4j的作者觉得jcl不好用,自己又写了一个新的接口api,那么就是slf4j。关于slf4j的集成图如下所示
理解slf4j日志门面了吗,它跟jcl机制不一样。 它就相当于这个游戏机, 我本身没有游戏, 只提供一个运行游戏的平台(门面) 要运行哪个游戏我不管, 你给我放哪块光盘我就运行哪个游戏。 JCL是自己去找,先找到哪个运行哪个
Slf4j与其他各种日志组件的桥接说明
jar包名 | 说明 |
---|---|
slf4j-log4j12-1.7.13.jar | Log4j1.2版本的桥接器,你需要将Log4j.jar加入Classpath。 |
log4j-slf4j-impl.jar | Log4j2版本的桥接器,还需要log4japi.jar log4jcore.jar |
slf4j-jdk14-1.7.13.jar | java.util.logging的桥接器,Jdk原生日志框架。 |
slf4j-nop-1.7.13.jar | NOP桥接器,默默丢弃一切日志。 |
slf4j-simple-1.7.13.jar | 个简单实现的桥接器,该实现输出所有事件到System.err. 只有Info以及高于该级别的消息被打印,在小 型应用中它也许是有用的。 |
slf4j-jcl-1.7.13.jar | Jakarta 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,至此,基础知识完毕,下面是实战!
注册完用户
通常会有这样的需求 将用户注册成功的信息 以短信
的形式发送给用户 一是为了验证用户手机号 是否合格 或者 是否能正确接收信息 二是将用户体验提升 还有可能会有积分加持
那么情况就来了!!!如果程序执行期间 积分加持
这块业务功能出错了 那么在 串行执行 这种模式中 可能造成我们用户注册失败 但是因为一个附属的功能 导致流失掉一个用户 这代价明显是很大的 作为解决方案 所以异步框架的好处就显而易见了!!!用户注册
功能异步
来处理 千万不要滥用
一定根据合适的业务场景来使用 就比如一些附属的功能 都能通过 异步
来完成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);
}
}
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"; } }
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(); } } }
localhost:8002
访问程序在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
容错问题
,如果送积分出现异常,不能因为送积分而导致用户注册失败。提升性能
,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。
SimpleAsyncTaskExecutor
线程池 它并不是一个实际意义上的线程池 事实上 它通常会给每个异步方法
新开一个线程 而这个线程被使用过后不会被回收异步方法
它会新开线程去执行 并不会重用线程 这样子并发量一大 服务器根本承受不住的哨兵
一样的东西(可以简单理解为哨兵
) 它负责监督线程池里面的状况 如果线程池里面空余线程剩余过多 且在规定时间内(一般是3-5分钟)没有使用 则会被销毁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; } }
异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)
在进行web开发时 我们通常会采用 get/post
方式(最常用 当然还有RestFul风格的DELETE PUT等等)进行访问controller接口
那么我们在实际开发中到底如何使用 如何去衡量呢?
这就需要我们得搞清Get
、Post
请求的区别了
区别
后端取值方式不同
url
中 可以通过 @RequestParam()
将参数值取出 而post 请求得数据都存在于 请求体中 也就是我们的RequestBody
所以我们需要添加 @RequestBody
来将数据取出get和post的缓存机制
get
方式如果使用于请求后端的静态资源 会将 静态资源 缓存一份在浏览器中 所以这就可以说明 为什么在请求后端的静态资源时一般使用的是 get
因为第二次进行请求时 直接从缓存中取出 快捷 效率高 所以我们思考一下 为什么在更新静态资源过后 有时需要清理浏览器的缓存?post
请求每次都需要去后端获取 每次的时间几乎都一致!! 网上有人证明过啦 可以去翻阅一下get比post请求更快
post
请求会将大量的数据包含在请求头内 而这些请求头需要先通过服务端的确认 这个确认也很直白 就是 requestBody
的接收结果 如果成功才将数据一一发送过来 这个过程是有一点耗时
且消耗性能
的
post
请求的过程:get
请求的过程:也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。
R类
Result
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类出去 这样子是不是就能解决我们的问题呢springmvc给我们提供了这样的支持 只需要实现 ResponseBodyAdvice
接口 重写方法 便能满足我们的需求
ResponseBodyAdvice
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); } }
说明
ResponseBodyAdvice
String
的话 要进行 特殊处理 使用市面上的一些Json工具包 将字符串转为Json即可 我使用的是 hutool-all<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.2</version>
</dependency>
如果不处理 String
的后果
Result.ok(o)
这句代码时 此时 o 已经确定好是String
它会调用 StringHttpMessageConverter
对它进行转换 它是 springmvc 将字符串传入前端时默认的转换器 作用于转换字符串为Json 然而这里已经被包装成Result
对象了 而StringHttpMessageConverter
只能转换 String
所以便报了错正确结果
@GetMapping("/ok")
public String ok() {
return "success";
}
try/catch
来处理异常 这样一来显得代码十分臃肿 二来处理程序十分繁琐 所以通常我们会采用 springmvc
内部提供的的统一异常处理 它是为了解决一系列错误的方案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; } }
将常量类升级使用枚举?
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); //出现错误后返回自己的错误信息 } }
注解说明:
RestControllerAdvice
和 ControllerAdvice
的区别
RestControllerAdvice
注解的类里面的方法在捕捉异常后是不能进行页面跳转的 而 ControllerAdvice
是可以的 也就是说和Controller
层的那两个注解意思有一点类似 RestController
和 Controller
ResponseStatus
INTERNAL_SERVER_ERROR
(内部服务器错误) 就是 500ExceptionHandler
@GetMapping("/error")
public String error() {
int num=1/0;
return null;
}
报完错以后进入 我们的全局异常里
符合我们的正确结果 但是问题来了 思考一下:如果所有controller方法出现了异常 那么都走全局异常 然后报 500
错误 这样子异常错误清晰吗?根据不清晰 对于我们开发这是很难的事情!!所以我们需要优化它。
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(); } }
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; } }
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();
}
//测试自定义异常
@GetMapping("/order")
public User order() {
throw new OrderException(ExceptionCodeEnum.ORDER_CREATE_FAIL);
}
通过swagger
页面进行访问
结果正是我们想要的
springmvc
也提供了支持 就是自定义异常
pojo
、普通的参数
等等 那么对于这些我们如何去校验呢 ?spring的 validator校验框架
遵守的是JSR-303
的验证规范(参数校验规范),JSR全称:Java Specification Requests缩写。
在默认情况下:SpringBoot会引入Hibernate Validation
机制来支持JSR-303验证规范。
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=) 被注释的元素必须在合适的范围内
在实际开发中,只需要三个步骤:
在需要校验的pojo,vo等等中的属性上增加对应注解,比如@NotBlank
在controller方法
参数中的 pojo,vo中加 @Validated
的注解即可。(普通参数有另外的处理方法)
使用全局统一异常处理捕获的验证失败的提示信息
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
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; }
//测试校验器
@PostMapping("/validate")
public UserVo validate(@RequestBody @Validated UserVo user) {
log.info("{}",user);
return user;
}
postman
等等console
swagger
如果你校验失败,springmvc的validator内部会以异常的方式进行返回。报错异常:MethodArgumentNotValidException
而这个异常里面,包含所有的校验的提示信息。
那么MethodArgumentNotValidException
我们可以通过异常的api 取出重要的信息
/**
* 对验证的统一异常进行统一处理
*/
@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;
}
通过异常的api取出所有异常字段对象 它是一个集合
将集合中所有的异常字段对象 最重要的信息取出 并存入map 使用json工具转化为字符串传入前端
@Validated
来验证吗? 并不能Assert
是 断言 的意思IllegalArgumentException
IllegalArgumentException
也得写一个自定义异常捕捉方法/**
* 普通参数校验统一处理
*/
@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;
}
//测试校验器
@PostMapping("/validate2")
public String validate2(@RequestBody String name) {
Assert.isNull(name,"用户名不能为空");
log.info("{}",name);
return name;
}
Validator 框架
给我们提供的 一些验证注解 并不够用 我们通常还有一些其他的需求 所以我们就需要自定义校验器来实现我们的需求@Email
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(); } }
ConstraintValidator
接口 该接口的作用就是 (源码得知:) 给 T
这个对象添加 A
的约束T
: String A
:@Phone
注解 给该String增加一个约束 该约束就是只能为@Phone
里面定义的规则 而Phone
依赖于PhoneValidator
在 PhoneValidator
里有一个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) { } }
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); } }
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。