002-SpringBoot
2022-03-15 18:22:26 0 举报
AI智能生成
002-SpringBoot 系统总结
作者其他创作
大纲/内容
Spring Boot 基础开发
Spring Boot 简介
Spring 的诞生
让开发变得简单,是 Spring 诞生的原动力。
EJB的缺点
Java 官方推出的企业级开发标准是 EJB
但 EJB 是相当臃肿、低效的,且难以测试,把当时的 Java 开发者折腾得不轻。
Rod Johnson 和 Spring 框架
那时候,国外有一个年轻的小伙 Rod Johnson,对 SSH 的繁琐产生了质疑。他不光质疑,还去做了他认为对的事情。
经过不断的经验总结和实践,他在 2004 年推出了经典力作《Expert one-on-one J2EE Development without EJB》。
该书奠定了 Spring 框架的思想基础,把 EJB 的种种缺点逐一否定,还提出了简洁的替代方案。
从此 Rod Johnson 和 Spring 框架一炮而红,其影响之深远,恐怕连 Rod Johnson 自己都想不到吧。
总结
有时候,不要过于迷信官方,也要敢于思考和质疑。
实践是检验真理的唯一标准,编程也不外乎是。
Spring 的发展
随着 Spring 的流行,Spring 团队也深感责任重大。
Spring 团队对 Spring 的优化工作也从未停歇,从 Spring1.x 到现在的 Spring5.x,每一个版本号都是进化的脚印。
SpringBoot的诞生
最开始,Spring 只支持基于 XML 的配置,后来又陆续增加了对注解配置、Java 类配置的支持。
但是无论怎么变换,都需要开发人员手工去配置,而这些配置往往千篇一律,令人乏味。
驾驶汽车,默认都是车窗关闭、空调关闭、仪表盘开启这样的设置。
如果每次进入汽车,都要手工逐一设置一遍,其实完全没有必要。
同理,既然大多数人开发 Spring 应用,都有默认的习惯。
那何不直接提供默认配置,项目启动时自动采用默认配置,只有当需要个性化功能时,再去手工配置。
所以,在 2014 年,一个叫 Spring Boot 的框架,就这么出现了。
实际使用中, Spring Boot 是高度封装的,开箱即用即可。在学习阶段,用不着了解很多的原理。
就像你开汽车,会挂挡就行,不需要知道变速箱是啥工作原理。
框架封装的目的就是为了傻瓜式使用, Spring Boot 就是这样的一个傻瓜式工具框架。等哪一天用得很溜了,再去研究原理也不迟。
Spring Boot 的由来/概念
Spring Boot 为简化 Spring 应用开发而生,Spring Boot 中的 Boot 一词,即为快速启动的意思。
Spring Boot 可以在零配置情况下一键启动,简洁而优雅。
为了让 Spring 开发者痛快到底,Spring 团队做了以下设计,基于以下设计目的,Spring 团队推出了 Spring Boot 。
Spring Boot是由Pivotal团队提供的全新框架,
其设计目的是用来简化新Spring应用的初始搭建以及开发过程。
该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。
通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者
SpringBoot的优点
1. 创建独立的Spring应用程序
2. 嵌入的Tomcat,无需部署WAR文件
3. 简化Maven配置
4. 自动配置Spring
5. 提供生产就绪型功能,如指标,健康检查和外部配置
6. 绝对没有代码生成和对XML没有要求配置
2. 嵌入的Tomcat,无需部署WAR文件
3. 简化Maven配置
4. 自动配置Spring
5. 提供生产就绪型功能,如指标,健康检查和外部配置
6. 绝对没有代码生成和对XML没有要求配置
简化依赖,提供整合的依赖项,告别逐一添加依赖项的烦恼;
简化配置,提供约定俗成的默认配置,告别编写各种配置的繁琐;
简化部署,内置 servlet 容器,开发时一键即运行。
可打包为 jar 文件,部署时一行命令即启动;
简化监控,提供简单方便的运行监控方式。
简化监控,提供简单方便的运行监控方式。
Spring Boot是多年来苦苦追寻的高效开发框架,理论上可开发各种程序应用,但尤其适合WEB开发。
Spring Boot自身集成的组件代码简洁、直观、开发便利、上手成本低、可靠性高、开发效果佳。
SpringBoot的优点
Spring Boot 的江湖地位
Spring Boot 框架已经是 Java 企业级应用开发的主流框架
由于 Spring Boot 设计优雅,实现简单,可以节省不少开发时间。
从此,程序员们有了更多时间去陪妹子逛街买裙子。
没有女朋友的小伙伴们,也有了更多时间思考追女孩的方案(一定要勇敢地行动呀)。
从一定程度上讲,Spring Boot 降低了程序员群体的单身比例。
Spring Boot 的火爆是必然的,据了解,Spring Boot 框架已经是 Java 企业级应用开发的主流框架了。
由于微服务的火爆,作为 Spring Cloud 实现基础的 Spring Boot ,更是春风得意,风头一时无两。
Spring Boot 的学习基础
Spring Boot 非常好用,但是并不是 0 基础就可以直接上手的。
Java 语言基础是必备的
对 Spring 及 Spring MVC 框架有一定的了解。
Spring MVC 是 Spring 大家庭的非常重要的一员
Spring Boot 是一个快速开发框架,其技术基础几乎全部来源自 Spring 。
Spring Boot 的技术需求背景
JSP + Servlet + JavaBean
MVC需全手动实现,大型商业项目基本无用,适合新手练习。
MVC需全手动实现,大型商业项目基本无用,适合新手练习。
SSH + 各种框架
配置极其繁琐,上手难,代码结构难以轻量化,移植与维护成本高。
配置极其繁琐,上手难,代码结构难以轻量化,移植与维护成本高。
SpringMVC + 各种框架(新能源平台)
核心为SpringMVC,再集成各种所需框架件,如Shiro,JDBCTemplate,Freemarker。配置多、初次集成繁琐。
核心为SpringMVC,再集成各种所需框架件,如Shiro,JDBCTemplate,Freemarker。配置多、初次集成繁琐。
Spring Boot
对应于所有技术需求,所有问题均有集成化组件,几乎没有配置,一键生成项目本体,自带线程级监控,上手轻易,可集成或不集成WEB容器运行。
对应于所有技术需求,所有问题均有集成化组件,几乎没有配置,一键生成项目本体,自带线程级监控,上手轻易,可集成或不集成WEB容器运行。
Spring Boot 第一个项目
Spring Boot 可以使用 Maven 构建,遵循 Maven 的项目结构规范,项目结构是模板化的,基本都一模一样。
模板化的东西可以自动生成,Spring 官方就提供了 Spring Initializr 。
使用Spring Initializr
网址 http://start.spring.io ,根据项目的情况填入以下信息。
创建项目 - https://start.spring.io/
它能自动生成 Spring Boot 项目,直接导入到开发工具使用即可。
1. 构建方式选择:此处选择 Maven Project 即可,表示生成的项目使用 Maven 构建。Spring Boot 项目亦可采用 Gradle 构建,目前 Spring Boot 主流的构建方式还是 Maven;
2. 编程语言选择:此处选择 Java 即可;
3. Spring Boot 版本选择: 2.x 版本与 1.x 版本还是有一些区别的,选择 2.x 新版本。此处虽然选择了 2.2.6 版本,但是由于 2.2.6 版本刚推出没多久,国内一些 Maven 仓库尚不支持。后面手工改为 2.2.5 版本,便于使用国内 Maven 仓库快速构建项目;
4. 所属机构设置:Group 表示项目所属的机构,就是开发项目的公司或组织。因为公司可能会重名,所以习惯上采用倒置的域名作为 Group 的值。
5. 项目标识设置:Artifact 是项目标识,用来区分项目。此处我们命名为 spring-boot-hello ,注意项目标识习惯性地采用小写英文单词,单词间加横杠的形式。比如 Spring Boot 官方提供的很多依赖,都是 spring-boot-starter-xxx 的形式;
6. 项目名称设置:Name 是项目名称,保持与 Artifact 一致即可;
7. 默认包名设置:Package name 是默认包名,保持默认即可;
8. 打包方式选择:此处选择将项目打包为 Jar 文件;
9. 添加项目依赖:此处不必修改,直接在 pom.xml 中添加依赖更加方便。注意 pom.xml 就是 Maven 的配置文件,可以指定项目需要引入的依赖;
10. 生成项目:点击 Generate 按钮,即可按我们设置的信息生成 Spring Boot 项目了。
2. 编程语言选择:此处选择 Java 即可;
3. Spring Boot 版本选择: 2.x 版本与 1.x 版本还是有一些区别的,选择 2.x 新版本。此处虽然选择了 2.2.6 版本,但是由于 2.2.6 版本刚推出没多久,国内一些 Maven 仓库尚不支持。后面手工改为 2.2.5 版本,便于使用国内 Maven 仓库快速构建项目;
4. 所属机构设置:Group 表示项目所属的机构,就是开发项目的公司或组织。因为公司可能会重名,所以习惯上采用倒置的域名作为 Group 的值。
5. 项目标识设置:Artifact 是项目标识,用来区分项目。此处我们命名为 spring-boot-hello ,注意项目标识习惯性地采用小写英文单词,单词间加横杠的形式。比如 Spring Boot 官方提供的很多依赖,都是 spring-boot-starter-xxx 的形式;
6. 项目名称设置:Name 是项目名称,保持与 Artifact 一致即可;
7. 默认包名设置:Package name 是默认包名,保持默认即可;
8. 打包方式选择:此处选择将项目打包为 Jar 文件;
9. 添加项目依赖:此处不必修改,直接在 pom.xml 中添加依赖更加方便。注意 pom.xml 就是 Maven 的配置文件,可以指定项目需要引入的依赖;
10. 生成项目:点击 Generate 按钮,即可按我们设置的信息生成 Spring Boot 项目了。
Spring Boot 项目结构分析
将下载的 zip 压缩包解压后导入开发工具,此处以 Eclipse 为例,依次点击 File-Import-Existing Maven Projects ,然后选择解压后的文件夹导入。
最外层的 spring-boot-wikis 表示工作集(working set),可以理解为项目分类。将 Spring Boot 学习项目都放入该工作集下,便于集中查看;
spring-boot-hello 是指定的项目名称;
src/main/java 是 Java 源代码目录,存放编写的 Java 代码;
src/main/resources 目录是静态资源目录,存放图片、脚本文件、配置文件等静态资源;
src/test/java 目录是测试目录,存放测试类。测试是非常重要的,从目录级别跟源代码同级,就能看出来测试的重要性;
target 目录存放打包生成的内容;
pom.xml 是项目的 Maven 配置文件,指定了项目的基本信息以及依赖项,Maven 就是通过配置文件得知项目构建规则的。
spring-boot-hello 是指定的项目名称;
src/main/java 是 Java 源代码目录,存放编写的 Java 代码;
src/main/resources 目录是静态资源目录,存放图片、脚本文件、配置文件等静态资源;
src/test/java 目录是测试目录,存放测试类。测试是非常重要的,从目录级别跟源代码同级,就能看出来测试的重要性;
target 目录存放打包生成的内容;
pom.xml 是项目的 Maven 配置文件,指定了项目的基本信息以及依赖项,Maven 就是通过配置文件得知项目构建规则的。
Spring Boot 可以在没有配置文件时照常运行。但如果需要个性化功能的话,就会用到配置文件了。
pom.xml 详解
因为 pom.xml 配置比较长,从头到尾分段解释下。
Maven 文档配置
这一段配置代码,其实是固定的格式,表示当前文档是 Maven 配置文档。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
</project>
Spring Boot 版本配置
这一段配置代码,指定使用 Spring Boot 2.2.5.RELEASE 版本 。
如果要更换 Spring Boot 版本,只需要修改 <version></version> 标签中间的版本号部分即可。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
项目信息配置
这一段配置代码,大家看到应该比较眼熟,内容即为之前使用 Spring Initializr 指定的项目信息。
其中,groupId 是机构标识、artifactId 是项目标识,version 是版本号,name 是项目名称,description 是项目的简单描述。
<groupId>com.imooc</groupId>
<artifactId>spring-boot-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-hello</name>
<description>Demo project for Spring Boot</description>
<artifactId>spring-boot-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-hello</name>
<description>Demo project for Spring Boot</description>
依赖配置
接下来,这一段代码配置,负责指定 Spring Boot 项目中需要的依赖。
Spring Boot 有一些起步依赖,形如 spring-boot-starter-xxx 的样式。
起步依赖整合了很多依赖项
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
可以看到上面两个依赖我们并没有指定版本号,其实是因为 Spring Boot 2.2.5 已经有默认的依赖项版本号了。
这是通过 Maven 父继承实现的,即 <parent> 标签配置部分,这个稍作了解即可。
插件配置
最后的这一段代码配置,指定了一个插件,用来构建、运行、打包 Spring Boot 项目。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Spring Boot 项目启动机制
Spring 与Spring Boot之间的关系
Spring 是什么?
Spring 中 XML / 注解 / Java 类三种配置方式,有什么区别和联系。
Spring 本质上是一个容器,里面存放的是 Java 对象,放入容器的 Java 对象被称为 Spring 组件(Bean)。
而 XML / 注解 / Java 类三种配置方式,只是形式不同,目的都是在容器中注册 Bean 。三种方式可以同时使用,只是需要注意, Bean 命名不要发生冲突。
当我们使用 Spring Boot 时会有变化吗?实际上,容器还是那个容器,配置也还是那三种配置。
当然 Spring Boot 本身就是为了简化配置,所以基本不再使用 XML 配置方式了。
Spring 与Spring Boot之间的关系
【容器化的思想】大海是鱼类的容器,地球是人类的容器,容器化的思想是普通存在的
打个比方, Spring 是插钥匙启动的轿车,而 Spring Boot 是无钥匙启动的轿车。
功能和原理是几乎一样的, Spring Boot 更加简单方便而已。
Spring Boot 是如何启动的
Spring Boot 项目生成后,只有简简单单一个类,简单优雅,赏心悦目!
@SpringBootApplication
public class SpringBootHelloApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHelloApplication.class, args);
}
}
public class SpringBootHelloApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHelloApplication.class, args);
}
}
public static void main 是普通的 main 方法,是程序执行的入口。
SpringApplication.run
看字面意思就知道,这是 Spring 应用的启动方法,运行该行代码后, Spring 应用就跑起来了。
这个方法有两个参数
args 是命令行参数,此处没啥作用;
另一个参数是 SpringBootHelloApplication.class ,包含类的信息。
这个类有啥信息啊?放眼看去,除了一个类名、一个静态方法外,并无其他。凭这些信息就能启动 Spring 应用?
注解 @SpringBootApplication
该注解是标注在类上的,属于类的信息。
Spring Boot 启动秘密就在这个注解上了。
神奇的 @SpringBootApplication 注解
看看这个注解到底是何方神圣!在 Eclipse 中选中该注解,按 F3 即可查看其定义。
示例
@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) })
public @interface SpringBootApplication {
}
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
其实就是一个组合注解,包含了多个注解的功能
SpringBoot的三个核心注解
@SpringBootConfiguration 注解
继承自 @Configuration 注解,功能也跟 @Configuration 一样。
它会将当前类标注为配置类了,在启动类中配置 Bean 就可以生效了。
拥有了 @SpringBootConfiguration ,我们就拥有了一个可以拿来即用的 Spring 容器环境了。
@ComponentScan 注解
用来指定要扫描的包,以便发现 Bean 。
注意在默认情况下, SpringBoot 扫描该注解标注类所在包及其子包。
当控制器、服务类等 Bean 放到不同的包中时,就需要通过 @ComponentScan 注解指定这些包,以便发现 Bean 。
@EnableAutoConfiguration 注解
用来启动自动配置。
开启自动配置后, Spring Boot 会扫描项目中所有的配置类,然后根据配置信息启动 Spring 容器。
Spring Boot 使用模板引擎开发 Web 项目
模板引擎是什么?
模板引擎这个词,咋听起来,有点高大上的意味。
实际上,模板引擎是非常平易近人的技术。譬如大家可能都比较熟悉的 JSP ,就是一种比较典型的模板引擎。
当浏览器将请求抛给控制器,控制器处理好数据后,就跳转 JSP 等模板引擎页面。
注意在跳转的同时,还会将数据组装好,也交给模板引擎处理。
模板引擎会根据数据,和模板引擎的规则,动态生成 HTML 页面,最后返回给浏览器显示。
模板引擎使用场景
两种使用 Spring Boot 开发 Web 项目的方式
第一种方式:前后端分离的方式
后端服务化的方式,也是当前主流方式。
前端是静态的 HTML 页面,通过 Ajax 请求 Spring Boot 的后端接口。
Spring Boot 返回数据一般采用 JSON 格式,前端接收后将数据显示。
第二种方式:采取模板引擎的方式。
前端的请求,到达 Spring Boot 的控制器后,控制器处理请求,然后将返回数据交给模板引擎。
模板引擎负责根据数据生成 HTML 页面,最后将 HTML 返回给浏览器。
前后端分离优点
【分工协助】后端可以按自己的进度开发接口,前端可以开发页面,需要时直接调用后端 API ;
【项目拓展】比如前期是做的网站,后续要加一个 APP ,后端接口可以直接复用;
【降低服务端压力】后端只提供数据,一部分业务逻辑在前端处理了。服务端要做的事情少了,自然压力就小。
模板引擎优点
【搜索引擎友好SEO】模板引擎开发的页面,对搜索引擎 SEO 比较友好
【开发速度快】简单的页面,用模板引擎开发速度比较快,毕竟模板化的方法,目的就是减少重复提高效率。
Spring Boot 中常用的模板引擎
Spring Boot 支持的模板引擎种类很多
常见的有 FreeMarker 、 Thymeleaf 、 JSP 。
因为这些模板引擎使用的用户都不少,所以我们逐一介绍下其实现过程。
至于孰优孰劣,请各位看官自行评价。
至于孰优孰劣,请各位看官自行评价。
正所谓:尺有所短,寸有所长,各取所爱,万物生长!
模板引擎主要负责通过一些模板标签,将控制器返回的数据解析为网页。
区别对比
FreeMarker 、Thymeleaf两种方式除了模板页面文件内容不同,其他地方基本都是一模一样的。
FreeMarker 和 Thymeleaf 的用法几乎是一模一样的,而 JSP 还需要手工添加一些目录和配置。
三种方式各有优劣, FreeMarker 模板语法比较简洁, Thymeleaf 可以直接使用 HTML 作为模板文件, JSP 用户群体广泛。
但是三种方式,都是一种模板引擎而已,将控制器返回数据转化为 HTML 页面显示,本质上没啥区别,大家对模板引擎有一个了解即可。
整体流程说明
本篇开发一个商品浏览项目实例。来思考下,实现商品浏览项目实例的整体流程:
思维保持连贯性和整体性,在做一些页面和方法较多的项目时,会感觉更加顺畅。
在做一个项目/模块时,不要一开始就动手写代码,最好是谋定而后动。
作为程序员,实际上是整个程序世界的总指挥。
应该先整体规划,再实现局部。
这种总分型的开发方法便于我们理顺思路,提高编码效率!
编码如行军,先了解地形,设定整体作战计划,再派兵执行任务
使用 FreeMarker
使用 Thymeleaf
使用JSP
注意 Spring Boot 官方已经不推荐使用 JSP 了,确实操作起来也比较麻烦。
但是由于 JSP 用户体量还是比较大的,所以此处还是简单演示下,开发步骤与 FreeMarker / Thymeleaf 基本一致。
Spring Boot 开发 RESTful 风格 Web 项目
前后端分离的产生背景
很多研发团队,可能都会有一个体会,当多人研发一个项目时,并不能达到 1+1>2 的效果。
有时候还会出现 1+1<1 ,即 2 个人还不如 1 个人干得快,甚是悲哀。
就比如我们要开发一个 Web 项目,由前端工程师和后端工程师共同完成。
前端工程师懂 HTML / CSS / JavaScript 和 Bootstrap 等前端技术与框架,但是几乎不懂后端 Java 语言。
后端工程师懂 Spring Boot 开发,略懂 HTML / CSS / JavaScript ,但是没用过前端框架。
后端工程师懂 Spring Boot 开发,略懂 HTML / CSS / JavaScript ,但是没用过前端框架。
这种情况,如果使用 FreeMarker / Thymeleaf / JSP 等模板引擎,将面临不小的困难。
前端工程师得先去学习模板引擎的规则,后端人员需要跟前端沟通控制器交给模板引擎处理的数据。
怎么办呢,不要担心,大佬们早就解决这个问题了。互联网江湖圈,遇到问题先莫慌,早有大佬为我们开好路!
前后端分离
前后端分离这种概念和技术,早就流行多年了。
具体内容
前端编写 HTML 页面,然后通过 Ajax 请求后端接口
后端把接口封装成 API ,返回 JSON 格式的数据;
前端接收到 JSON 返回数据后渲染到页面。
前后端分离的好处
前端工程师根本不需要懂后端,调用后端接口就行。
后端使用 Spring Boot 控制器返回 JSON 十分简单,给方法添加个注解,就能将返回值序列化为 JSON 。
前端干前端的活,后端干后端的活,职责分明,界限明确。这就是前后端分离的好处啊!
RESTful 风格后端接口
做后端接口的公司这么多,如果大家都定义自己的规范,是不利于公司之间的合作的。
如果大家都能遵循一个规范来开发接口,很明显相关人员都能省心不少。
前后端分离时,后端接口可不能太随意,目前后端接口编写大多遵循 RESTful 风格。
RESTful 就是一种非常流行的 HTTP 接口规范,简单明了,使用的人也多,用它准没错。
规范的意义,就是提供标准,提高效率。
汽车行业标准化程度已经很高了,软件行业还需努力!
Tips: RESTful 风格通过 HTTP 动词( GET / POST / PUT / DELETE )区分操作类型, URL 格式比较固定,仅此而已,非常简单。
整体流程说明
本节实现一个基于 RESTful 风格的 Spring Boot 商品浏览 API 实例。
做事之前,先定整体流程。凡事预则立,不预则废,老祖宗的智慧太厉害了,我们争取发扬光大。
根据需求制定 RESTful 风格的接口文档
既然是要做商品浏览页面,将商品增删改查都实现了就是了。
RESTful 风格接口并不麻烦,一般情况下需要项目团队一起商量制定。
此处指定如下
指定如下接口文档
使用 Spring Boot 开发 RESTful 风格的 Web 项目,是当前主流的 Web 项目开发模式
这种开发模式的特点
简单
简单的目的就是提高开发效率;
简单主要是依赖 Spring Boot 实现
明确
明确的目的是统一规范,从而降低沟通成本,最终也是为了提高开发效率。
明确就是靠 RESTful 风格来规范。
按文档开发后端 API 接口
相比于使用模板引擎,用 Spring Boot 开发后端接口简直太轻松了。
通过给控制器添加注解,就能将控制器方法返回值序列化为 JSON 。程序员最爱,就是轻松又愉快。
RESTful 项目其实就是标准的 Web 项目,引入 Web 项目依赖即可。
<!-- 引入web项目相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
使用 Postman 测试 API 接口可用
后端开发完 API 接口后,需要先进行下简单测试以保证接口是正确可用的。
可以使用 Postman 进行简单测试。启动 Spring Boot 项目后,使用 Postman 进行可视化测试。
Spring Boot整合Swagger2实现自动化测试与文档
Swagger2产生背景
使用 Spring Boot 后,开发人员心里美美的,再也不需要写一大堆的配置文件了。
痛点
(1)每次开发完后端接口,使用 Postman 测试比较麻烦。差不多的接口地址,差不多的参数,每次测试都要输入一遍,挺烦心。
(2)前端完全不懂后端技术,天天要文档。就这么简简单单几个接口,还得给前端写。
(3)由于开发阶段经常会修改接口,所以编写纸质文档实在是劳民伤财。
能不能自动生成接口文档,然后自动生成测试界面呢?百度一搜 "Spring Boot 接口自动化测试",发现很多文章推荐使用 Swagger2 。
一般公司在开发时,可以使用 Swagger2 快速实现测试,并生成在线文档。
如果使用 Swagger2 ,重启 Spring Boot 后刷新页面,就能看到最新 API 文档。
可开启热部署。
当代码发生变化时, Spring Boot 会自动重启,这样就省得每次都要手工重启 SpringBoot 。
<!-- 引入该依赖即可开启热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
通过引入依赖,简单配置,就能实现一个非常棒的功能
这就是 Spring Boot 开箱即用的优点体现。
Swagger2 功能总览
Swagger2 可以识别控制器中的方法,然后自动生成可视化的测试界面。
后端开发人员编写完 Spring Boot 后端接口后,直接可视化测试就行了。
无需借助 Postman 等工具,也无需编写测试类和测试方法,更无需联系前端开发确认接口是否正常。
如果给控制器方法添加注解,还能自动生成在线 API 文档,简直太省心了。
Spring Boot 中使用 Swagger2 流程
引入 Swagger2 相关依赖
修改 pom.xml 文件,引入 Swagger2 相关依赖。
<!-- 添加swagger2相关功能 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 添加swagger-ui相关功能 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 添加swagger-ui相关功能 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
启用并配置 Swagger2 功能
加一个配置类,专门用于配置 Swagger2 相关功能,这样比较清晰点。
通过 @EnableSwagger2 注解开启 Swagger2 功能
通过 @Bean 标注的方法,将对 Swagger2 功能的设置放入容器。
@Configuration // 告诉Spring容器,这个类是一个配置类
@EnableSwagger2 // 启用Swagger2功能
public class Swagger2Config {
/**
* 配置Swagger2相关的bean
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com"))// com包下所有API都交给Swagger2管理
.paths(PathSelectors.any()).build();
}
/**
* 此处主要是API文档页面显示信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("演示项目API") // 标题
.description("学习Swagger2的演示项目") // 描述
.termsOfServiceUrl("http://www.imooc.com") // 服务网址,一般写公司地址
.version("1.0") // 版本
.build();
}
}
@EnableSwagger2 // 启用Swagger2功能
public class Swagger2Config {
/**
* 配置Swagger2相关的bean
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com"))// com包下所有API都交给Swagger2管理
.paths(PathSelectors.any()).build();
}
/**
* 此处主要是API文档页面显示信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("演示项目API") // 标题
.description("学习Swagger2的演示项目") // 描述
.termsOfServiceUrl("http://www.imooc.com") // 服务网址,一般写公司地址
.version("1.0") // 版本
.build();
}
}
使用 Swagger2 进行接口测试
此时我们启动项目,然后访问 http://127.0.0.1:8080/swagger-ui.html ,即可打开自动生成的可视化测试页面,如下图。
Swagger2 自动生成可视化测试界面略
感觉这个页面简单整洁,直接给测试人员使用都很方便。
Swagger2 将接口以可视化的方式呈现出来,开发人员不必手输接口地址、参数名称,就可以发起测试并查看结果,确实非常方便。
后端人员在开发完成后,可以自己使用 Swagger2 测试下接口可行性。而前端人员也可以打开 Swagger2 网页直接验证接口功能。
使用 Swagger2 生成在线 API 文档
使用 Swagger2 生成在线文档比较简单,直接在控制器方法上添加注解即可。
@Api(tags = "商品API") // 类文档显示内容
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
@ApiOperation(value = "根据id获取商品信息") // 接口文档显示内容
@GetMapping("/goods/{id}")
public GoodsDo getOne(@PathVariable("id") long id) {
return goodsService.getGoodsById(id);
}
@ApiOperation(value = "获取商品列表") // 接口文档显示内容
@GetMapping("/goods")
public List<GoodsDo> getList() {
return goodsService.getGoodsList();
}
@ApiOperation(value = "新增商品") // 接口文档显示内容
@PostMapping("/goods")
public void add(@RequestBody GoodsDo goods) {
goodsService.addGoods(goods);
}
@ApiOperation(value = "根据id修改商品信息") // 接口文档显示内容
@PutMapping("/goods/{id}")
public void update(@PathVariable("id") long id, @RequestBody GoodsDo goods) {
goods.setId(id);
goodsService.editGoods(goods);
}
@ApiOperation(value = "根据id删除商品") // 接口文档显示内容
@DeleteMapping("/goods/{id}")
public void delete(@PathVariable("id") long id) {
goodsService.removeGoods(id);
}
}
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
@ApiOperation(value = "根据id获取商品信息") // 接口文档显示内容
@GetMapping("/goods/{id}")
public GoodsDo getOne(@PathVariable("id") long id) {
return goodsService.getGoodsById(id);
}
@ApiOperation(value = "获取商品列表") // 接口文档显示内容
@GetMapping("/goods")
public List<GoodsDo> getList() {
return goodsService.getGoodsList();
}
@ApiOperation(value = "新增商品") // 接口文档显示内容
@PostMapping("/goods")
public void add(@RequestBody GoodsDo goods) {
goodsService.addGoods(goods);
}
@ApiOperation(value = "根据id修改商品信息") // 接口文档显示内容
@PutMapping("/goods/{id}")
public void update(@PathVariable("id") long id, @RequestBody GoodsDo goods) {
goods.setId(id);
goodsService.editGoods(goods);
}
@ApiOperation(value = "根据id删除商品") // 接口文档显示内容
@DeleteMapping("/goods/{id}")
public void delete(@PathVariable("id") long id) {
goodsService.removeGoods(id);
}
}
此时再次打开 http://127.0.0.1:8080/swagger-ui.htm ,会发现相关接口都已经有文字描述了。
Spring Boot 配置文件详解
为什么SpringBoot说好的零配置,又需要修改配置文件呢?
Spring Boot 可以在零配置的情况下使用,但是不代表 Spring Boot 完全不需要配置文件。
举例
Spring Boot 开发的 Web 项目默认的启动端口是 8080 。
如果我们想更换启动端口,通过配置文件去修改是比较好的。
如果放到代码中,修改一个端口都要重新编译下程序,岂不烦哉?
配置文件不是必须的,但是如果想实现一些个性化的功能,还是需要用到配置文件的。
开发 Spring Boot 项目时,如果默认配置满足不了我们的需求,可以通过手工配置组件实现我们需要的功能。
这些组件可能是各个公司提供的,根据相应文档,为其配置各个属性即可。
构建演示 Web 项目(略)
修改项目启动配置
运行启动类,启动Springboot的应用,控制台提示如下
Tomcat started on port(s): 8080 (http) with context path ''
可以看出, Spring Boot 应用默认启动端口是 8080 ,默认项目路径是空。
通过修改 resources/application.properties 来自定义项目启动配置:
# 启动端口
server.port=8000
# 项目路径
server.servlet.context-path=/spring-boot-profile
server.port=8000
# 项目路径
server.servlet.context-path=/spring-boot-profile
再次启动应用,控制台提示变为
Tomcat started on port(s): 8000 (http) with context path '/spring-boot-profile'
此时项目对应的访问路径为: http://127.0.0.1:8000/spring-boot-profile , 使用浏览器访问效果如下:
浏览器显示返回数据
Spring Boot 支持两种格式的配置文件
即 .properties 文件和 .yml 配置文件。
这两种配置文件没有本质区别,只是格式不同。
.properties 配置
使用 顿号 分割语义
.yml 配置
server:
port: 8000
servlet:
context-path: /spring-boot-profile
port: 8000
servlet:
context-path: /spring-boot-profile
使用 缩进 分割语义
自定义配置项
可以在配置文件中,使用自定义配置
举例
例如我们开发了一个微信公众号后台应用,需要在程序中配置公众号的 appid 和 secret 。
配置文件如下:
# 公众号appid
wxmp.appid=111
# 公众号secret
wxmp.secret=222
wxmp.appid=111
# 公众号secret
wxmp.secret=222
定义一个组件,通过 @Value 注解,注入配置项的值。
实例:
//微信公众号参数
@Component//注册为组件
public class WxMpParam {
@Value("${wxmp.appid}")//注入wxmp.appid配置项
private String appid;
@Value("${wxmp.secret}")//注入wxmp.secret配置项
private String secret;
//省略get set方法
}
@Component//注册为组件
public class WxMpParam {
@Value("${wxmp.appid}")//注入wxmp.appid配置项
private String appid;
@Value("${wxmp.secret}")//注入wxmp.secret配置项
private String secret;
//省略get set方法
}
通过控制器测试配置项是否注入成功。
实例
@RestController
public class HelloController {
@Autowired
private WxMpParam wxMpParam;
@GetMapping("/hello")
public Map hello() {
Map<String, String> map = new HashMap<String, String>();
map.put("appid",wxMpParam.getAppid());
map.put("secret",wxMpParam.getSecret());
return map;
}
}
public class HelloController {
@Autowired
private WxMpParam wxMpParam;
@GetMapping("/hello")
public Map hello() {
Map<String, String> map = new HashMap<String, String>();
map.put("appid",wxMpParam.getAppid());
map.put("secret",wxMpParam.getSecret());
return map;
}
}
配置项自动注入对象(设定对象与配置项的对应关系)
如果参数很多,一一指定对象属性和配置项的关联非常麻烦。
可以通过设定对象与配置项的对应关系,来实现配置项的自动注入。
举例
@Component // 注册为组件
@EnableConfigurationProperties // 启用配置自动注入功能
@ConfigurationProperties(prefix = "wxmp") // 指定类对应的配置项前缀
public class WxMpParam {
private String appid;// 对应到wxmp.appid
private String secret; // 对应到wxmp.secret
//省略 get set
}
@EnableConfigurationProperties // 启用配置自动注入功能
@ConfigurationProperties(prefix = "wxmp") // 指定类对应的配置项前缀
public class WxMpParam {
private String appid;// 对应到wxmp.appid
private String secret; // 对应到wxmp.secret
//省略 get set
}
在上面的代码中,通过 prefix = "wxmp" 指定了关联配置的前缀
属性 appid 即关联到前缀 + 属性名为 wxmp.appid 的配置项。
同理,属性 secret 关联到 wxmp.secret 配置项。
配置文件中使用随机数(启动多个客户端)
配置文件中使用随机数也是比较常见的场景
尤其启动多个客户端时,希望指定一个启动端口的范围,例如 10 - 20 ,可配置如下:
实例
# 配置端口为1-20间的随机数
server.port=${random.int[10,20]}
server.port=${random.int[10,20]}
这样可以连续启动四个客户端,启动端口分别是 12 、 13 、 17 、 19 ,可见是随机的,而且在指定的范围内波动。
自定义配置文件(将配置分到不同文件中)
有时候参数太多,都放到一个配置文件中太乱了,希望将配置分到不同文件中,然后每个文件保存不同配置。
例如微信公众号配置,单独建立一个 wxmp.properties 文件,内容如下:
# wxmp.properties配置文件
# 公众号的appid
wxmp.appid=111
# 公众号的secret
wxmp.secret=222
# 公众号的appid
wxmp.appid=111
# 公众号的secret
wxmp.secret=222
WxMpParam 代码如下
//微信公众号参数
@Component // 注册为组件
@PropertySource(value = "classpath:wxmp.properties", encoding = "utf-8") // 指定配置文件及编码
public class WxMpParam {
@Value("${wxmp.appid}")
private String appid;
@Value("${wxmp.secret}")
private String secret;
}
@Component // 注册为组件
@PropertySource(value = "classpath:wxmp.properties", encoding = "utf-8") // 指定配置文件及编码
public class WxMpParam {
@Value("${wxmp.appid}")
private String appid;
@Value("${wxmp.secret}")
private String secret;
}
配置项引用(引用其他配置项的值)
Spring Boot 配置项是可以引用其他配置项的值的
实例
# wxmp.properties
# 公众号的appid
wxmp.appid=111
# 公众号的secret,值为111222
wxmp.secret=${wxmp.appid}222
# 公众号的appid
wxmp.appid=111
# 公众号的secret,值为111222
wxmp.secret=${wxmp.appid}222
根据不同场景,灵活使用不同的配置文件机制
如果配置项比较少,直接全部写在 application.properties 。
如果配置项很多,可以划分为若干配置文件。
如果很多自定义配置拥有相同的前缀,可以指定前缀,让配置项自动注入对象中。
Spring Boot 跨域与前后端分离
前后端分离的模式介绍
目前,使用 Spring Boot 进行前后端分离项目开发,应该是主流做法了。
这种方式,在开发、测试阶段,都比较方便。
开发阶段,项目组定义好接口规范后,前端按规范开发前端页面,后端按规范编写后端接口,职责分明。
测试阶段,后端是独立项目,可以进行单元测试。前端可以随时使用最新版本的后端程序进行实际测试。
测试阶段,后端是独立项目,可以进行单元测试。前端可以随时使用最新版本的后端程序进行实际测试。
前后端分离的模式,有着很多的优越性,所以造就了它的流行。
前后端分离说明
越大型的项目,越需要规范,越需要职责分明。
形成详细的 API 文档说明
一般在项目开发过程中,会先根据功能需求规划页面和后端 API 接口, API 接口往往会形成详细的 API 文档说明。
后端快速地开发 API 接口
后端会快速地开发 API 接口,前期并不会将 API 功能完全实现,而是仅仅返回一些测试值。
就像本篇文章中后端接口返回的博客列表,并不是真实从数据库查询出来的,而是构造的测试数据。
前端根据 API 文档,编写前端界面
前后端通过 API 文档完成沟通
前后端实际上互相不必直接沟通,
他们之间的交流是通过 API 文档完成的
同期,前端可以根据 API 文档,编写前端界面,并调用后端 API 进行测试。
前后端在理解上并不会存在很多问题
由于后端接口往往采用 RESTful 标准规范,所以在理解上并不会存在很多问题。
Spring Boot 对后端服务化的支持
Spring Boot 对后端服务化的支持实际上是相当到位的
使用几个简单的注解,就能轻松构建独立的后端服务项目。
技术选型
本篇通过商品浏览项目实例,展现前后端分离项目的开发、测试全流程。
技术选型方面,
后端选择 Spring Boot
接口风格采用 RESTful 标准。
前端则使用简单的 HTML + Bootstrap + jQuery ,并通过 jQuery 的 $.ajax 方法访问后端接口。
后端开发流程
使用 Spring Initializr 构建一个 Spring Boot 应用
修改 pom.xml
引入 Web 项目依赖,开启热部署便于修改测试。
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
定义商品类、商品服务类、商品控制器类
GoodsDo
GoodsService
GoodsController
在控制器类中编写获取商品列表的接口供前端调用。
启动应用
访问 http://127.0.0.1:8080/goods ,返回内容如下,可见后端接口已经可用。
子主题
前端开发流程
前后端分离开发,实际上前端工作就简化了。
直接新建项目文件夹 shop-front (商城前端项目文件夹),然后将前端页面放到该文件夹即可。
注意该页面不需要放到 Spring Boot 项目目录下,随便找个目录放置即可。实际开发过程中,后端和前端的项目可能都不在一台计算机上。
前端核心业务代码如下,由于前端技术不是本节介绍的重点,所以不再详细解释
使用浏览器双击打开,查看控制台发现有错误信息提示
开发完该页面后,直接使用浏览器双击打开,查看控制台发现有错误信息提示。
浏览器控制台返回错误信息
关键是 has been blocked by CORS policy ,意味着被 CORS 策略阻塞了。
前端页面请求被 CORS 阻塞了,所以没成功获取到后端接口返回的数据。
CORS 跨域介绍(同源策略)
同源策略
跨域实际上源自浏览器的同源策略
所谓同源,指的是协议、域名、端口都相同的源(域)。
浏览器会阻止一个域的 JS脚本,向另一个不同的域发出的请求
这也是为了保护浏览器的安全。
CORS是什么?
跨域资源共享
一种允许跨域 HTTP 请求的机制
回到例子
例子中,发起请求的网页与请求资源的 URL 协议、域名、端口均不同,所以该请求就被浏览器阻止了。
在这种情况下就要想办法实现 CORS 跨域了。
Spring Boot 跨域的实现
跨域的方法有很多种,此处演示一种常用的跨域方法。
添加一个配置类CorsConfig
@Configuration//配置类
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")//对所有请求路径
.allowedOrigins("*")//允许所有域名
.allowCredentials(true)//允许cookie等凭证
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")//允许所有方法
.maxAge(3600);
}
};
}
}
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")//对所有请求路径
.allowedOrigins("*")//允许所有域名
.allowCredentials(true)//允许cookie等凭证
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")//允许所有方法
.maxAge(3600);
}
};
}
}
通过上面的配置类,实现了允许所有对该 Spring Boot 的请求跨域。
此时再次打开网页,被跨域策略阻塞的提示消失
Spring Boot 打包与部署
项目开发完毕后,免不了将前后端应用打包,然后部署到生产服务器上运行。
本篇就演示一个标准的打包、部署过程。以上一篇开发的前后端分离项目 spring-boot-cors 为例进行打包、部署演示,步骤如下:
服务器运行环境安装
操作系统
一般服务器采用 Linux 或者 Windows Server 系统,
相对而言 Linux 系统更加稳定安全
实际上 Windows Server 系统,对于一般应用来说也足够了
本篇使用 Windows Server 系统进行演示。
云服务器
推荐使用云服务器,更加稳定且易于维护
国内厂商阿里云、华为云都还不错。
云服务器的硬盘读写性能非常重要
IOPS值
它是衡量硬盘读写性能的一个指标
一般建议:要采用 IOPS > 3800 的云磁盘。
在购买云服务器时务必关注下云硬盘的 IOPS 值
具备云服务器后,需要安装 JDK 以便运行 Spring Boot 应用。
由于 nginx 对静态资源的负载能力非常强悍,所以将前端应用部署到 nginx 上。
Spring Boot 打包为 jar 并运行(默认打包方式)
打 jar 包
Spring Boot 应用可以打包为 war 或者 jar ,官方和我个人都是推荐打 jar 包。
可以直接运行,无需部署到 Web 服务器上。
打开命令行工具,进入 spring-boot-cors 项目目录后
运行 mvn clean package -Dmaven.test.skip=true 命令
即可快速打包 Spring Boot 应用。
下图中的 jar 文件,即为打包后的 Spring Boot 应用。
打包后生成的文件内容
运行
接下来将该应用拷贝至服务器,在同一目录下新建 start.bat 文件
java -jar spring-boot-cors-0.0.1-SNAPSHOT.jar
双击 start.bat 文件即可启动项目,效果如下,可以看出系统已经启动成功(started)。
Spring Boot 打包项目已启动
Spring Boot 打包为 war 并运行
也可将 Spring Boot 打包为 war ,然后放置于 Tomcat 的 webapps 目录下加载运行
打包为 war 的过程
修改pom文件中默认打包方式
显式指定打包方式为 war
<groupId>com.imooc</groupId>
<artifactId>spring-boot-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-cors</name>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<artifactId>spring-boot-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-cors</name>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
在打包时需要排除内置的 Tomcat
由于 Spring Boot 内置了 Tomcat ,所以在打包时需要排除内置的 Tomcat ,
避免内置 Tomcat 和 war 包部署运行的 Tomcat 产生冲突。
在 pom.xml 中添加如下依赖即可:
<!--打war包时排除内置的tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
继承 SpringBootServletInitializer 类并重写 configure 方法
然后,还需要继承 SpringBootServletInitializer 类并重写 configure 方法
目的:,告诉 Tomcat 当前应用的入口在哪
@SpringBootApplication
public class SpringBootCorsApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringBootCorsApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootCorsApplication.class, args);
}
}
public class SpringBootCorsApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringBootCorsApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootCorsApplication.class, args);
}
}
子主题
使用 mvn clean package -Dmaven.test.skip=true 命令打包应用
最后,即可同样使用 mvn clean package -Dmaven.test.skip=true 命令打包应用了
运行命令后会在 target 目录下生成 war 文件
将该文件放置于 Tomcat 的 webapps 目录下运行即可。
前端应用部署
前端应用的部署更加简单
直接在云服务器上下载 nginx , 然后解压
打开网址 http://nginx.org/en/download.html
点击下图中的链接下载即可。
nginx 下载链接
下载解压后,将前端页面直接放到 nginx/html 目录下即可。
当然如果有很多网页,可以先在该目录下建立子目录,便于归类网页。
建立 shop-front 目录,然后将网页放入其中
shop-front 目录(表示商城系统的前端项目)
商城系统前端项目目录内容
修改 goods.html 中访问的后端 URL 地址
还需要修改 goods.html 中访问的后端 URL 地址
假设云服务器的公网 IP 为 x.x.x.x ,则修改为
$.ajax({
type: "GET",
url: "http://x.x.x.x:8080/goods", //后端接口地址
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (res) {
$.each(res, function (i, v) {
row = "<tr>";
row += "<td>" + v.id + "</td>";
row += "<td>" + v.name + "</td>";
row += "<td>" + v.price + "</td>";
row += "<td>" + v.pic + "</td>";
row += "</tr>";
$("#goodsTable").append(row);
});
},
error: function (err) {
console.log(err);
}
});
type: "GET",
url: "http://x.x.x.x:8080/goods", //后端接口地址
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (res) {
$.each(res, function (i, v) {
row = "<tr>";
row += "<td>" + v.id + "</td>";
row += "<td>" + v.name + "</td>";
row += "<td>" + v.price + "</td>";
row += "<td>" + v.pic + "</td>";
row += "</tr>";
$("#goodsTable").append(row);
});
},
error: function (err) {
console.log(err);
}
});
解释下后端地址 http://x.x.x.x:8080/goods
HTTP 代表协议
x.x.x.x 代表云服务器公网地址
8080 是后端项目的启动端口,
由于我们没有在配置文件中设置,所以默认就是 8080
最后 goods 是控制器中设定的后端接口路径。
启动 nginx,访问 http://x.x.x.x
双击 nginx.exe 启动 nginx
由于 nginx 默认启动端口是 80 ,所以此时访问 http://x.x.x.x
效果如下,说明 nginx 启动成功!
nginx 已启动成功
测试验证
现在后端 Spring Boot 应用已启动,前端项目也通过 nginx 启动起来。
在浏览器地址栏打开 http://x.x.x.x/shop-front/goods.html ,效果如下,说明的项目全部部署成功。
项目部署成功后页面显示效果
Spring Boot 数据访问
Spring Boot 使用 JdbcTemplate
前言
如果项目非常简单,仅仅是对数据库几张表进行简单的增删改查操作,那么实际上直接使用 JDBC 操作数据库就可以了。
由于 JDBC 中有很多模板代码,每次都是加载驱动-建立数据库连接-查询或操作数据库-关闭数据库连接这样的模板代码
Spring 提供了 JdbcTemplate 对原生 JDBC 进行了简单的封装。
本篇文章,实现一个完整的、基于 Spring Boot + JdbcTemplate + MySQL 的商品管理项目实例。
技术选型
数据库使用 MySQL ,商品信息存储到商品表内即可。
后端项目使用 Spring Boot ,通过控制器暴露 RESTful 风格的接口供前端调用,通过 JdbcTemplate 实现对数据库的操作。
前端项目使用 Bootstrap 开发,通过 jQuery 提供的 $.ajax 方法访问后端接口。
后端项目使用 Spring Boot ,通过控制器暴露 RESTful 风格的接口供前端调用,通过 JdbcTemplate 实现对数据库的操作。
前端项目使用 Bootstrap 开发,通过 jQuery 提供的 $.ajax 方法访问后端接口。
数据库模块实现
只需要一张商品表,保存商品相关的信息即可。我们使用 Navicat 新建数据库 shop ,并在其中新建数据表 goods 。
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
默认值最好不要采用 NULL , NULL 会影响索引的效率,而且在查询时需要用 is null 或 is not null 筛选,容易被忽略。
Spring Boot 后端实现
新建一个 Spring Boot 项目,通过 JdbcTemplate 访问数据库,同时接口依旧采用 RESTful 风格。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-jdbctemplate
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。
由于本项目需要访问数据库,所以引入 spring-boot-starter-jdbc 依赖和 mysql-connector-java 依赖。
pom.xml 文件中依赖项如下:
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
建立项目结构
依次新建以下类结构
GoodsDo:商品类,对应 goods 商品表;
GoodsDao:商品数据访问类,用于访问数据库;
GoodsService:商品服务类,用于封装对商品的操作;
GoodsController:商品控制器类,用于对外提供 HTTP 接口;
CorsConfig:跨域配置类,允许前端页面跨域访问后端接口。
GoodsDo:商品类,对应 goods 商品表;
GoodsDao:商品数据访问类,用于访问数据库;
GoodsService:商品服务类,用于封装对商品的操作;
GoodsController:商品控制器类,用于对外提供 HTTP 接口;
CorsConfig:跨域配置类,允许前端页面跨域访问后端接口。
此时项目目录如下
项目目录
开发商品类 GoodsDo
开发商品数据访问类
商品数据访问类 GoodsDao 是本篇的重点,通过注入 JdbcTemplate 类型的组件,实现数据库操作。注入代码如下:
//商品数据库访问类
@Repository // 标注数据访问类
public class GoodsDao {
@Autowired
private JdbcTemplate jdbcTemplate;
}
@Repository // 标注数据访问类
public class GoodsDao {
@Autowired
private JdbcTemplate jdbcTemplate;
}
由于已引入了 spring-boot-starter-jdbc 依赖,所以 Spring Boot 项目已自动配置了 JdbcTemplate 组件,拿来即用即可
此时启动应用,发现报错信息
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
没有合适的数据库驱动、也没有合适的 URL 属性。
Spring Boot 自动配置JdbcTemplate之余,还在尝试自动配置数据源 DataSource
数据源 DataSource,即 JdbcTemplate 要操作的真实数据库信息。
报错信息已经提示,没有合适的数据库驱动、也没有合适的 URL 属性。
配置数据源信息 DataSource
只需要通过配置文件指定数据源信息, Spring Boot 就可以识别配置,并加载到数据源组件中。
JdbcTemplate 也可以自动识别该数据源,从而实现对数据库的操作。
配置数据源文件 与注意点
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=123456
指定了编码方式
在 URL 配置中指定了编码方式
防止出现数据库中文乱码情况。
指定了时区
指定了时区为北京时间所在的东八区(GMT%2B8)
避免因时区问题,导致错误。
此时再次启动 Spring Boot 应用,正常运行,说明我们的数据源配置生效了。
通过 JdbcTemplate 操作数据库(GoodsDao)
通过 JdbcTemplate 进行增删改查操作非常简洁,
Spring 官方封装了原生 JDBC 中冗余的模板代码,使数据库访问操作更加简洁
开发商品服务类(GoodsService)
商品服务类比较简单
直接调用 GoodsDao 完成商品服务方法封装即可。
发商品控制器类GoodsController
遵循之前的 RESTful 风格,制定后端访问接口如下:
后端访问接口
根据上面的接口列表,实现控制器类代码
GoodsController
开发跨域配置类
由于我们是前后端分离的项目开发方式,所以需要为 Spring Boot 添加跨域配置类:
//跨域配置类
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")// 对所有请求路径
.allowedOrigins("*")// 允许所有域名
.allowCredentials(true)// 允许cookie等凭证
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH")// 允许所有方法
.maxAge(3600);
}
};
}
}
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")// 对所有请求路径
.allowedOrigins("*")// 允许所有域名
.allowCredentials(true)// 允许cookie等凭证
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH")// 允许所有方法
.maxAge(3600);
}
};
}
}
前端页面开发
本节主要介绍 Spring Boot 中 JdbcTemplate 的用法,所以前端页面仅给出代码和注释,不再进行详细介绍了。
前端只有一个页面,使用 Bootstrap 的样式和插件,通过 jQuery 的 $.ajax 方法访问后端接口,逻辑并不复杂。
此处简单展示下浏览商品部分的前端代码,感兴趣的同学可以从 Git仓库 查看完整代码。
前端只有一个页面,使用 Bootstrap 的样式和插件,通过 jQuery 的 $.ajax 方法访问后端接口,逻辑并不复杂。
此处简单展示下浏览商品部分的前端代码,感兴趣的同学可以从 Git仓库 查看完整代码。
项目效果
直接使用浏览器打开前端页面,效果如下
浏览商品
总结
基于 Spring Boot 自动装配的功能,只需要引入相应的依赖,编写必要的数据库参数配置,即可直接使用 JdbcTemplate 。
所谓开箱即用,就是只需必要的操作,就可以直接到达可用的境界。
Spring Boot 集成 Druid 数据源
前言
本篇使用 Druid 替换默认的数据源,然后做一下性能对比测试。
网上有很多文章写 Druid 性能如何如何强悍,但是很多并没有事实依据。
做程序员还是要严谨,相信实践是检验真理的唯一标准。
所以本篇的内容就是研究下 Druid 如何使用,及其性能到底是否足够优异。
数据源的作用
数据源实际上是一个接口 javax.sql.DataSource 。
Spring Boot 在操作数据库时,是通过数据源类型的组件实现的。
数据源的类型 与 Druid 数据源
数据源的类型有很多,但是都实现了 javax.sql.DataSource 接口,所以 Spring Boot 可以尽情地更换各类数据源以实现不同的需求。
其中 Druid 数据源就是数据源中比较出类拔萃的存在,而且是阿里开发的。
国人出品的东西,咱们能支持的必须得支持啊,当然这是建立在人家做的确实好的基础上。
Druid 数据源的好处
在实际研发与生产测试过程中,使用 Druid 的情况还是非常多的
Druid 非常稳定、性能也表现相当优异,更重要的是它提供了全面直观的监控手段
所以现阶段还是推荐大家使用 Druid 。
使用默认数据源(HikariDataSource)
什么是HikariDataSource
Spring Boot 2.2.5 版本使用的默认数据源是 HikariDataSource
该数据源号称拥有全世界最快的数据库连接池
准备数据库
使用之前的商城数据库(shop)及商品信息数据表(goods),表结构如下:
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
使用 Spring Initializr 创建项目
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 , Group 为 com.imooc , Artifact 为 spring-boot-hikari ,
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。
由于本项目需要访问数据库,所以引入 spring-boot-starter-jdbc 依赖和 mysql-connector-java 依赖。
pom.xml 文件中依赖项如下:
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
构建商品类和商品数据访问类
定义商品类,对应商品表:
GoodsDo
定义商品数据库访问类:
GoodsDao
配置数据源信息
通过配置文件,设置数据源信息
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
Spring Boot的单元测试与输出结果
@SpringBootTest 注解
注意,Spring Boot 进行测试时,需要添加注解 @SpringBootTest 。
添加注解后,该类可以直接通过 @Test 标注的方法发起单元测试,容器环境都已准备好,非常方便。
通过@SpringBootTest注解,开启测试类功能,当测试方法启动时,启动了Spring容器
通过测试类发起测试,此处简单执行 1000 次插入,看看执行时间。
实例
@SpringBootTest
class SpringBootHikariApplicationTests {
@Autowired
private DataSource dataSource;// 自动注入数据源
@Autowired
private GoodsDao goodsDao;
//打印数据源信息
@Test // 测试方法
void printDataSource() {
System.out.println(dataSource);
}
//批量插入测试
@Test
void insertBatch() {
// 开始时间
long startTime = System.currentTimeMillis();
// 执行1000次插入
GoodsDo goods = new GoodsDo();
goods.setName("测试");
goods.setPic("测试图片");
goods.setPrice("1.0");
for (int i = 0; i < 1000; i++) {
goodsDao.insert(goods);
}
// 输出操作时间
System.out.println("use time:" + (System.currentTimeMillis() - startTime)+"ms");
}
}
class SpringBootHikariApplicationTests {
@Autowired
private DataSource dataSource;// 自动注入数据源
@Autowired
private GoodsDao goodsDao;
//打印数据源信息
@Test // 测试方法
void printDataSource() {
System.out.println(dataSource);
}
//批量插入测试
@Test
void insertBatch() {
// 开始时间
long startTime = System.currentTimeMillis();
// 执行1000次插入
GoodsDo goods = new GoodsDo();
goods.setName("测试");
goods.setPic("测试图片");
goods.setPrice("1.0");
for (int i = 0; i < 1000; i++) {
goodsDao.insert(goods);
}
// 输出操作时间
System.out.println("use time:" + (System.currentTimeMillis() - startTime)+"ms");
}
}
输出结果如下
默认数据源类型为 HikariDataSource
插入 1000 条数据的时间大概为 1500ms
(注意时间可能跟电脑性能等很多因素相关,此处只是进行简单的对比测试)。
use time:1518ms
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.HikariDataSource
使用 Druid 数据源
使用 Druid 数据源进行对比测试
准备数据库
同上
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 , Group 为 com.imooc , Artifact 为 spring-boot-druid
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。
由于本项目需要访问数据库,所以引入 spring-boot-starter-jdbc 依赖和 mysql-connector-java 依赖
由于使用 Druid ,所以还需要添加 Druid 相关依赖。
pom.xml 文件中依赖项
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- springboot druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- springboot druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
构建商品类和商品数据访问类
同上
配置数据源信息
通过配置文件,设置数据源信息。
由于我们不再使用默认数据源,所以此处需要指定数据源类型为 DruidDataSource 。
# 指定数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
Spring Boot的单元测试与输出结果
测试类代码同 spring-boot-hikari 一致
运行测试类后,结果如下
use time:1428ms
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceWrapper
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceWrapper
对比结果分析
其实只能得出一个结论,在某些场景下 Druid 的速度不比 Hikari 慢,甚至还略胜一筹。
当然我们只是对两种数据源的默认配置、单一线程情况进行了简单测试,
Druid 监控
为什么要使用Druid?
说了半天,也没看出 Druid 好在哪儿啊,为啥还费劲将默认的 Hikari 更换掉呢。
Druid 是为监控而生,说明 Druid 最强大的功能实际上是监控
如何实现 Druid 监控
添加监控相关的配置类
Druid配置代码
@Configuration
public class DruidConfig {
//注册servletRegistrationBean
@Bean
public ServletRegistrationBean servletRegistrationBean() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),
"/druid/*");
servletRegistrationBean.addInitParameter("allow", "");
// 账号密码
servletRegistrationBean.addInitParameter("loginUsername", "imooc");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
servletRegistrationBean.addInitParameter("resetEnable", "true");
return servletRegistrationBean;
}
//注册filterRegistrationBean
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
// 添加过滤规则.
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
public class DruidConfig {
//注册servletRegistrationBean
@Bean
public ServletRegistrationBean servletRegistrationBean() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),
"/druid/*");
servletRegistrationBean.addInitParameter("allow", "");
// 账号密码
servletRegistrationBean.addInitParameter("loginUsername", "imooc");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
servletRegistrationBean.addInitParameter("resetEnable", "true");
return servletRegistrationBean;
}
//注册filterRegistrationBean
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
// 添加过滤规则.
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
注意设定了监控功能的账号和密码。
访问验证
此时打开网址 http://127.0.0.1:8080/druid
即可显示 Druid 登录页面
Druid 登录页面
使用指定的用户名 imooc 密码 123456 登录后,即可查看各类监控信息,内容还是非常全面的,此处就不再展开介绍了。
Druid 监控页面
Spring Boot 使用 JPA
前言
使用 JDBC ,或者 JdbcTemplate 操作数据库,需要编写大量的 SQL 语句。
SQL 语句基本都是些模板代码,实际上是可以通过分析模板代码的规则自动生成的。
JPA 就是简化 Java 持久层数据操作的技术标准,是一种方案和规范。
最开始是 Sun 公司提出的, Sun 公司就是开发出 Java 的公司,一度非常厉害,结果被 Oracle 收购了。
Sun 公司虽然提出了 JPA 标准,但是并没有具体实现。
JPA 的实现里面比较出名的就是 Hibernate 了,所以本篇我们也是以 Hibernate 实现为基础进行 Spring Boot + JPA 的实例讲解。
Spring Data JPA - https://spring.io/projects/spring-data-jpa
本篇演示一个 Spring Boot 商品管理项目实例,其中数据持久层操作采用 JPA ,以体会 JPA 的优雅与高效。
核心知识点:
默认由Hibernate实现ORM,同时支持与其它结合
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
JpaRepository
注解
@Entity、@Table、@Id、@columnDefinition、@Column(name = “XXX", columnDefinition = “XXX")
分页
Pageable
条件查询,关联查询
Specifications
JPA常规操作关键词
JPA常规操作关键词
JPA 基本原理
在开始实例之前,还是有必要聊聊 JPA 是如何实现的,便于大家理解。
首先是 ORM 映射,通过注解或 XML 描述对象和表直接的映射关系。例如 GoodsDo 商品类对应数据库中的 goods 商品表,商品类里面的属性和商品表里面的列一一对应,商品类的一个对象就对应商品表中的一行数据。
然后就是对数据库进行 CRUD (增删改查)操作了,由于已经配置了对象和表的映射关系,所以可以自动生成对应的 SQL 语句,然后执行语句即可。
首先是 ORM 映射,通过注解或 XML 描述对象和表直接的映射关系。例如 GoodsDo 商品类对应数据库中的 goods 商品表,商品类里面的属性和商品表里面的列一一对应,商品类的一个对象就对应商品表中的一行数据。
然后就是对数据库进行 CRUD (增删改查)操作了,由于已经配置了对象和表的映射关系,所以可以自动生成对应的 SQL 语句,然后执行语句即可。
开发流程
使用 Spring Boot + JPA 开发一个完整实例。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-jpa
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。
由于本项目需要使用 JPA 访问数据库,所以引入 spring-boot-starter-jdbc 、 mysql-connector-java 和 spring-boot-starter-data-jpa 依赖。
pom.xml 文件中依赖项
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- myql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
修改配置文件
在 application.properties 中添加以下配置:
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
# 启动时更新表结构,保留数据
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
# 启动时更新表结构,保留数据
spring.jpa.hibernate.ddl-auto=update
此处需要注意的是 spring.jpa.hibernate.ddl-auto=update 。
以理解项目启动时,根据实体类结构更新数据库表结构,且保留数据库中的数据。
因为设置了 spring.jpa.hibernate.ddl-auto=update , JPA 会在项目启动时自动建立表结构。
开发商品类 (GoodsDo)
开发商品类 GoodsDo ,并通过注解实现类结构与数据表结构的映射。
//商品类
@Entity // 表示这是一个数据对象类
@Table(name = "goods") // 对应数据库中的goods表
public class GoodsDo {
//商品id
@Id // 该字段对应数据库中的列为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增长
@Column(name = "id") // 对应goods表中的id列
private Long id;
//商品名称
@Column(name = "name") // 对应goods表中的name列
private String name;
//商品价格
@Column(name = "price") // 对应goods表中的price列
private String price;
//商品图片
@Column(name = "pic") // 对应goods表中的pic列
private String pic;
// 省略get set方法
}
@Entity // 表示这是一个数据对象类
@Table(name = "goods") // 对应数据库中的goods表
public class GoodsDo {
//商品id
@Id // 该字段对应数据库中的列为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增长
@Column(name = "id") // 对应goods表中的id列
private Long id;
//商品名称
@Column(name = "name") // 对应goods表中的name列
private String name;
//商品价格
@Column(name = "price") // 对应goods表中的price列
private String price;
//商品图片
@Column(name = "pic") // 对应goods表中的pic列
private String pic;
// 省略get set方法
}
开发数据操作接口(IGoodsDao)
开发商品数据接口,代码如下:
//商品数据操作接口
@Repository
public interface IGoodsDao extends CrudRepository<GoodsDo, Long> {
}
@Repository
public interface IGoodsDao extends CrudRepository<GoodsDo, Long> {
}
@Repository
将接口标注为数据访问层组件
该接口通过继承 CrudRepository 实现 CRUD 操作。
泛型参数分别为实体类及主键的数据类型。
注意,此时已经可以通过 IGoodsDao 对数据库 goods 表进行增删改查操作了。
开发服务层(GoodsService)
开发 Goods Service ,注入 IGoodsDao 类型组件实现服务方法。
代码
//商品服务类
@Service
public class GoodsService {
@Autowired
private IGoodsDao goodsDao;
// 新增商品
public void add(GoodsDo goods) {
goodsDao.save(goods);
}
//删除商品
public void remove(Long id) {
goodsDao.deleteById(id);
}
//编辑商品信息
public void edit(GoodsDo goods) {
goodsDao.save(goods);
}
//按id获取商品信息
public Optional<GoodsDo> getById(Long id) {
return goodsDao.findById(id);
}
//获取商品信息列表
public Iterable<GoodsDo> getList() {
return goodsDao.findAll();
}
}
@Service
public class GoodsService {
@Autowired
private IGoodsDao goodsDao;
// 新增商品
public void add(GoodsDo goods) {
goodsDao.save(goods);
}
//删除商品
public void remove(Long id) {
goodsDao.deleteById(id);
}
//编辑商品信息
public void edit(GoodsDo goods) {
goodsDao.save(goods);
}
//按id获取商品信息
public Optional<GoodsDo> getById(Long id) {
return goodsDao.findById(id);
}
//获取商品信息列表
public Iterable<GoodsDo> getList() {
return goodsDao.findAll();
}
}
Optional 类
此处需要解释下 Optional 类,它是一个包装类。
它的内容是空或者包含的对象,所以可以避免空指针问题。
开发控制器(GoodsController)
还是遵循 RESTful 风格,开发控制器类。
//商品控制器类
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
// 按id获取商品信息
@GetMapping("/goods/{id}")
public Optional<GoodsDo> getOne(@PathVariable("id") long id) {
return goodsService.getById(id);
}
//获取商品列表
@GetMapping("/goods")
public Iterable<GoodsDo> getList() {
return goodsService.getList();
}
//新增商品
@PostMapping("/goods")
public void add(@RequestBody GoodsDo goods) {
goodsService.add(goods);
}
//编辑商品
@PutMapping("/goods/{id}")
public void update(@PathVariable("id") long id, @RequestBody GoodsDo goods) {
// 修改指定id的博客信息
goods.setId(id);
goodsService.edit(goods);
}
//移除商品
@DeleteMapping("/goods/{id}")
public void delete(@PathVariable("id") long id) {
goodsService.remove(id);
}
}
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
// 按id获取商品信息
@GetMapping("/goods/{id}")
public Optional<GoodsDo> getOne(@PathVariable("id") long id) {
return goodsService.getById(id);
}
//获取商品列表
@GetMapping("/goods")
public Iterable<GoodsDo> getList() {
return goodsService.getList();
}
//新增商品
@PostMapping("/goods")
public void add(@RequestBody GoodsDo goods) {
goodsService.add(goods);
}
//编辑商品
@PutMapping("/goods/{id}")
public void update(@PathVariable("id") long id, @RequestBody GoodsDo goods) {
// 修改指定id的博客信息
goods.setId(id);
goodsService.edit(goods);
}
//移除商品
@DeleteMapping("/goods/{id}")
public void delete(@PathVariable("id") long id) {
goodsService.remove(id);
}
}
测试
主要是测试 JPA 模块正确可用,所以直接在测试类发起对 IGoodsDao 方法的测试即可。
使用 JPA 后,最大的好处就是不用写 SQL 了,完全面向对象编程,简洁又省心,何乐而不为。
新增测试
首先建立数据库 shop ,数据库中不必有表 goods ,如果有 goods 表的话,可以将它删除。
因为设置了 spring.jpa.hibernate.ddl-auto=update , JPA 会在项目启动时自动建立表结构。
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaAddTest {
@Autowired
private IGoodsDao goodsDao;
//新增测试
@Test
public void testAdd() {
GoodsDo goods = new GoodsDo();
goods.setName("梨张");
goods.setPic("梨图片");
goods.setPrice("2.0");
GoodsDo result = goodsDao.save(goods);
System.out.println("新增商品id:" + result.getId());
assertNotNull(result);
}
}
@SpringBootTest
public class JpaAddTest {
@Autowired
private IGoodsDao goodsDao;
//新增测试
@Test
public void testAdd() {
GoodsDo goods = new GoodsDo();
goods.setName("梨张");
goods.setPic("梨图片");
goods.setPrice("2.0");
GoodsDo result = goodsDao.save(goods);
System.out.println("新增商品id:" + result.getId());
assertNotNull(result);
}
}
运行测试类,控制台输出新增商品id:1,说明插入一条数据成功,且插入数据 id 为 1 。
同时查看数据库,发现已经自动构建表结构:
同时查看数据库,发现已经自动构建表结构:
MySQL 数据库已自动构建表结构
修改测试
当调用 save 方法,如果给参数中 id 属性赋值,则会进行数据更新操作
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaEditTest {
@Autowired
private IGoodsDao goodsDao;
//修改测试
@Test
public void testEdit() {
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("梨张");
goods.setPic("梨图片");
goods.setPrice("100.0");
GoodsDo result = goodsDao.save(goods);
assertNotNull(result);
}
}
@SpringBootTest
public class JpaEditTest {
@Autowired
private IGoodsDao goodsDao;
//修改测试
@Test
public void testEdit() {
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("梨张");
goods.setPic("梨图片");
goods.setPrice("100.0");
GoodsDo result = goodsDao.save(goods);
assertNotNull(result);
}
}
此时查看数据库中数据,发现金额已修改成功。
MySQL 数据库中金额修改成功
查询测试
进行按 id 查询、查询所有操作,并打印查询结果。
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaQueryTest {
@Autowired
private IGoodsDao goodsDao;
//按id查询
@Test
public void testQueryById() {
Optional<GoodsDo> goodsOptional = goodsDao.findById(1L);
GoodsDo goods = goodsOptional.get();
System.out.println(goods.getId() + "-" + goods.getName() + "-" + goods.getPic() + "-" + goods.getPrice());
}
//查询全部
@Test
public void testQueryAll() {
Iterable<GoodsDo> goodsIt = goodsDao.findAll();
for (GoodsDo goods : goodsIt) {
System.out.println(goods.getId() + "-" + goods.getName() + "-" + goods.getPic() + "-" + goods.getPrice());
}
}
}
@SpringBootTest
public class JpaQueryTest {
@Autowired
private IGoodsDao goodsDao;
//按id查询
@Test
public void testQueryById() {
Optional<GoodsDo> goodsOptional = goodsDao.findById(1L);
GoodsDo goods = goodsOptional.get();
System.out.println(goods.getId() + "-" + goods.getName() + "-" + goods.getPic() + "-" + goods.getPrice());
}
//查询全部
@Test
public void testQueryAll() {
Iterable<GoodsDo> goodsIt = goodsDao.findAll();
for (GoodsDo goods : goodsIt) {
System.out.println(goods.getId() + "-" + goods.getName() + "-" + goods.getPic() + "-" + goods.getPrice());
}
}
}
删除测试
指定删除 id 为 1 的商品。
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaRemoveTest {
@Autowired
private IGoodsDao goodsDao;
//删除测试
@Test
public void testRemove() {
goodsDao.deleteById(1L);
}
}
@SpringBootTest
public class JpaRemoveTest {
@Autowired
private IGoodsDao goodsDao;
//删除测试
@Test
public void testRemove() {
goodsDao.deleteById(1L);
}
}
运行后,数据库中商品信息被删除,大功告成!
Spring Boot 集成 MyBatis
本篇就演示下,如何在 Spring Boot 框架中快速集成并使用 MyBatis 。
Hibernate 和 MyBatis的区别
企业级应用数据持久层框架,最常见的应该是 Hibernate 和 MyBatis 。
Hibernate
Hibernate 是相当彻底的 ORM 对象 - 关系映射框架
使用 Hibernate ,开发者可以不考虑 SQL 语句的编写与执行,直接操作对象即可。
MyBatis
与 Hibernate 相比, MyBatis 还是需要手工编写 SQL 语句的。
恰好由于互联网行业数据量非常巨大,对 SQL 性能有比较苛刻的要求,往往都需要手工编写 SQL 。
在此背景下, MyBatis 逐渐流行。
除此之外,MyBatis 是更加简单,更容易上手的框架,但是功能也是相对简陋点。
MyBatis 可以自由的编写 SQL 语句,开发人员可以充分发挥 SQL 语句的性能。
MyBatis 还有一些简化开发的工具和框架,可以简化 MyBatis 开发过程,在一定程度上提高开发效率。
MyBatis-Plus
MyBatis-Generator
实例场景
本篇使用 Spring Boot 与 MyBatis ,开发一个商城系统中商品管理模块后端部分。
依然遵循 Restful 风格,以便快速理解与接入。
Spring Boot 中使用 MyBatis 操作数据库十分方便
引入相关依赖后
定义数据访问接口
通过映射文件描述对象 - 关系映射即可
通过 MapperScan 注解扫描数据访问接口所在的包,以便发现和注册相关的组件。
数据库模块实现
新建数据库 shop ,其中包含商品表:
表结构定义如下
CREATE TABLE `goods` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '唯一编号',
`name` varchar(255) DEFAULT '' COMMENT '商品名称',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '商品价格',
`pic` varchar(255) DEFAULT '' COMMENT '图片文件名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
Spring Boot 后端实现
接下来,可以开发 Spring Boot 后端项目了,并使用 MyBatis 作为数据持久层框架。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-mybatis ,生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。
由于本项目需要访问数据库,所以引入 spring-boot-starter-jdbc 依赖和 mysql-connector-java 依赖。
由于项目中使用了 MyBaits ,所以还需要引入 mybatis-spring-boot-starter 依赖。
本节实例开发完成后会使用 JUnit 进行测试,所以引入 junit 依赖。
pom.xml 文件中依赖项如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
数据源配置
修改 application.properties 文件,配置数据源信息。
Spring Boot 会将数据源自动注入到 MyBatis 的 sqlSessionFactory 组件中。
对于开发者来说,这一切都是自动实现的, MyBatis 同样可以开箱即用,简单到爆炸。
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
开发数据对象类(GoodsDo)
开发 goods 表对应的数据对象类 GoodsDo
//商品类
public class GoodsDo {
//商品id
private Long id;
//商品名称
private String name;
//商品价格
private String price;
//商品图片
private String pic;
// 省略 get set方法
}
public class GoodsDo {
//商品id
private Long id;
//商品名称
private String name;
//商品价格
private String price;
//商品图片
private String pic;
// 省略 get set方法
}
开发数据访问层
数据访问层直接使用接口实现即可,接口中添加商品的增删改查基本操作。
//商品数据库访问接口
@Repository // 标注数据访问组件
public interface GoodsDao {
//新增商品
public int insert(GoodsDo Goods);
//删除商品(根据id)
public int delete(Long id);
//修改商品信息(根据id修改其他属性值)
public int update(GoodsDo Goods);
//查询商品信息(根据id查询单个商品信息)
public GoodsDo selectOne(Long id);
// 查询商品列表
public List<GoodsDo> selectAll();
}
@Repository // 标注数据访问组件
public interface GoodsDao {
//新增商品
public int insert(GoodsDo Goods);
//删除商品(根据id)
public int delete(Long id);
//修改商品信息(根据id修改其他属性值)
public int update(GoodsDo Goods);
//查询商品信息(根据id查询单个商品信息)
public GoodsDo selectOne(Long id);
// 查询商品列表
public List<GoodsDo> selectAll();
}
添加 @MapperScan 注解
修改 Spring Boot 配置类,添加 @MapperScan 注解,扫描数据访问接口所在的包,
@SpringBootApplication
@MapperScan("com.imooc.springbootmybatis") // 指定MyBatis扫描的包,以便将数据访问接口注册为bean
public class SpringBootMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
@MapperScan("com.imooc.springbootmybatis") // 指定MyBatis扫描的包,以便将数据访问接口注册为bean
public class SpringBootMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
添加 MyBatis 映射文件
编写数据访问层接口之后,MyBatis 需要知道,如何将接口方法及参数转换为 SQL 语句,以及 SQL 语句执行结果如何转换为对象。
这些都是通过映射文件描述的, MyBatis 映射文件就是描述对象 - 关系映射的配置文件。
首先通过 application.properties 指定映射文件的位置:
# 指定MyBatis配置文件位置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.mapper-locations=classpath:mapper/*.xml
然后在 resources/mapper 目录下新建 GoodsMapper.xml 文件
该文件就是 goods 表对应的映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应GoodsDao接口 -->
<mapper namespace="com.imooc.springbootmybatis.GoodsDao">
<!-- 对应GoodsDao中的insert方法 -->
<insert id="insert" parameterType="com.imooc.springbootmybatis.GoodsDo">
insert into goods (name,price,pic) values (#{name},#{price},#{pic})
</insert>
<!-- 对应GoodsDao中的delete方法 -->
<delete id="delete" parameterType="java.lang.Long">
delete from goods where id=#{id}
</delete>
<!-- 对应GoodsDao中的update方法 -->
<update id="update" parameterType="com.imooc.springbootmybatis.GoodsDo">
update goods set name=#{name},price=#{price},pic=#{pic} where id=#{id}
</update>
<!-- 对应GoodsDao中的selectOne方法 -->
<select id="selectOne" resultMap="resultMapBase" parameterType="java.lang.Long">
select <include refid="sqlBase" /> from goods where id = #{id}
</select>
<!-- 对应GoodsDao中的selectAll方法 -->
<select id="selectAll" resultMap="resultMapBase">
select <include refid="sqlBase" /> from goods
</select>
<!-- 可复用的sql模板 -->
<sql id="sqlBase">
id,name,price,pic
</sql>
<!-- 保存SQL语句查询结果与实体类属性的映射 -->
<resultMap id="resultMapBase" type="com.imooc.springbootmybatis.GoodsDo">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="price" property="price" />
<result column="pic" property="pic" />
</resultMap>
</mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应GoodsDao接口 -->
<mapper namespace="com.imooc.springbootmybatis.GoodsDao">
<!-- 对应GoodsDao中的insert方法 -->
<insert id="insert" parameterType="com.imooc.springbootmybatis.GoodsDo">
insert into goods (name,price,pic) values (#{name},#{price},#{pic})
</insert>
<!-- 对应GoodsDao中的delete方法 -->
<delete id="delete" parameterType="java.lang.Long">
delete from goods where id=#{id}
</delete>
<!-- 对应GoodsDao中的update方法 -->
<update id="update" parameterType="com.imooc.springbootmybatis.GoodsDo">
update goods set name=#{name},price=#{price},pic=#{pic} where id=#{id}
</update>
<!-- 对应GoodsDao中的selectOne方法 -->
<select id="selectOne" resultMap="resultMapBase" parameterType="java.lang.Long">
select <include refid="sqlBase" /> from goods where id = #{id}
</select>
<!-- 对应GoodsDao中的selectAll方法 -->
<select id="selectAll" resultMap="resultMapBase">
select <include refid="sqlBase" /> from goods
</select>
<!-- 可复用的sql模板 -->
<sql id="sqlBase">
id,name,price,pic
</sql>
<!-- 保存SQL语句查询结果与实体类属性的映射 -->
<resultMap id="resultMapBase" type="com.imooc.springbootmybatis.GoodsDo">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="price" property="price" />
<result column="pic" property="pic" />
</resultMap>
</mapper>
测试
直接编写测试类,对数据访问接口进行测试。
@FixMethodOrder(MethodSorters.NAME_ASCENDING) 注解
@FixMethodOrder是控制@Test方法执行顺序的注解
@FixMethodOrder有三种选择
MethodSorters.JVM 按照JVM得到的顺序执行 即按照代码顺序执行
MethodSorters.NAME_ASCENDING 按照方法名字顺序执行
MethodSorters.DEFAULT 按照默认顺序执行 以确定的但是不可预期的顺序执行
MethodSorters.NAME_ASCENDING 按照方法名字顺序执行
MethodSorters.DEFAULT 按照默认顺序执行 以确定的但是不可预期的顺序执行
使用MethodSorters.NAME_ASCENDING ,这是使用方法名称排名的,即按照ASCLL码值逐个比较方法名,排序执行
此处通过 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 注解,使测试方法按名称顺序依次执行。
这样就可以一次性测试 GoodsDao 中的所有方法了,
具体测试代码如下
//GoodsDao测试类
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING) // 按方法名称顺序测试
class GoodsDaoTest {
@Autowired
private GoodsDao goodsDao;
//新增一个商品
@Test
void test_01() {
GoodsDo goods = new GoodsDo();
goods.setName("手机");
goods.setPic("phone.jpg");
goods.setPrice("2000");
int count = goodsDao.insert(goods);
assertEquals(1, count);// count值为1则测试通过
}
//更新商品信息
@Test
void test_02() {
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("手机");
goods.setPic("phone.jpg");
goods.setPrice("3000");
int count = goodsDao.update(goods);
assertEquals(1, count);// count值为1则测试通过
}
//获取商品信息
@Test
void test_03() {
GoodsDo goods = goodsDao.selectOne(1L);
assertNotNull(goods);// goods不为null则测试通过
}
// 删除商品
@Test
void test_04() {
int count = goodsDao.deletex(1L);//此处应为delete(1L)
assertEquals(1, count);// count值为1则测试通过
}
//获取商品信息列表
@Test
void test_05() {
List<GoodsDo> goodsList = goodsDao.selectAll();
assertEquals(0, goodsList.size());// goodsList.size()值为0则测试通过
}
}
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING) // 按方法名称顺序测试
class GoodsDaoTest {
@Autowired
private GoodsDao goodsDao;
//新增一个商品
@Test
void test_01() {
GoodsDo goods = new GoodsDo();
goods.setName("手机");
goods.setPic("phone.jpg");
goods.setPrice("2000");
int count = goodsDao.insert(goods);
assertEquals(1, count);// count值为1则测试通过
}
//更新商品信息
@Test
void test_02() {
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("手机");
goods.setPic("phone.jpg");
goods.setPrice("3000");
int count = goodsDao.update(goods);
assertEquals(1, count);// count值为1则测试通过
}
//获取商品信息
@Test
void test_03() {
GoodsDo goods = goodsDao.selectOne(1L);
assertNotNull(goods);// goods不为null则测试通过
}
// 删除商品
@Test
void test_04() {
int count = goodsDao.deletex(1L);//此处应为delete(1L)
assertEquals(1, count);// count值为1则测试通过
}
//获取商品信息列表
@Test
void test_05() {
List<GoodsDo> goodsList = goodsDao.selectAll();
assertEquals(0, goodsList.size());// goodsList.size()值为0则测试通过
}
}
测试结果如下
说明所有测试都通过了。
JUnit 测试结果
Spring Boot 使用事务
事务的重要性
工作中确实碰到过一些不知道使用事务的朋友,毫无疑问会给项目带来一些风险。
举例
网购时,需要扣减库存,同时生成订单。
如果扣库存成功了,没生成订单,结果是库存不知道为何变少了;
如果生成订单了,没扣库存,那就有可能卖出去的数量比库存还多。
这两种情况都是不能接受的,必须保证这两个对数据库的更新操作,同时成功,或者同时失败。
事务就是这样一种机制,将对数据库的一系列操作视为一个执行单元,保证单元内的操作同时成功,或者当有一个操作失败时全部失败。
实例场景
在 Spring Boot 中使用事务非常简单
本小节通过商品扣减库存、生成订单的实例,演示下 Spring Boot 中使用事务的具体流程。
数据库模块实现
需要有一个商品表,保存商品的唯一标识、名称、库存数量,结构如下:
CREATE TABLE `goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
`name` varchar(255) DEFAULT NULL COMMENT '商品名称',
`num` bigint(255) DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
`name` varchar(255) DEFAULT NULL COMMENT '商品名称',
`num` bigint(255) DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
购买商品后还需要生成订单,保存订单唯一标识、购买商品的 id 、购买数量。
CREATE TABLE `order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`count` bigint(20) DEFAULT NULL COMMENT '购买数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`count` bigint(20) DEFAULT NULL COMMENT '购买数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Spring Boot 后端实现
接下来,开始开发 Spring Boot 后端项目,并且使用事务实现扣减库存、生成订单功能。
数据库访问部分使用比较流行的 MyBatis 框架。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-transaction
生成项目后导入 Eclipse 开发环境。
引入项目依赖POM文件
引入热部署依赖、 Web 依赖、数据库访问相关依赖及测试相关依赖
具体如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
数据源配置
修改 application.properties 文件,配置数据源信息。
# 配置数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置数据库用户名
spring.datasource.username=root
# 配置数据库密码
spring.datasource.password=Easy@0122
开发数据对象类(GoodsDo、OrderDo)
开发 goods 表对应的数据对象类 GoodsDo
然后开发 order 表对应的数据对象类 OrderDo
开发数据访问层(GoodsDao、OrderDao)
定义商品数据访问接口(GoodsDao)
//商品数据库访问接口
@Repository // 标注数据访问组件
public interface GoodsDao {
//查询商品信息(根据id查询单个商品信息)
public GoodsDo selectForUpdate(Long id);
//修改商品信息(根据id修改其他属性值)
public int update(GoodsDo Goods);
}
@Repository // 标注数据访问组件
public interface GoodsDao {
//查询商品信息(根据id查询单个商品信息)
public GoodsDo selectForUpdate(Long id);
//修改商品信息(根据id修改其他属性值)
public int update(GoodsDo Goods);
}
实现查询剩余库存与扣减库存功能。
注意,在查询商品剩余库存时,采用面向对象的方法
将对应 id 的商品信息全部取出,更加方便点。
采用 selectForUpdate 命名
表示该方法使用了 select ... for update 的 SQL 语句查询方式
以锁定数据库对应记录,规避高并发场景下库存修改错误问题。
同样 update 方法也采用了面向对象的方式,根据 id 修改其他信息,方便复用。
定义订单数据访问接口(OrderDao)
// 订单数据库访问接口
@Repository // 标注数据访问组件
public interface OrderDao {
/*/新增订单
public int insert(OrderDo order);
}
@Repository // 标注数据访问组件
public interface OrderDao {
/*/新增订单
public int insert(OrderDo order);
}
实现生成订单的功能
修改 Spring Boot 配置类,添加 @MapperScan 注解
@SpringBootApplication
@MapperScan("com.imooc.springboottransaction") // 指定MyBatis扫描的包,以便将数据访问接口注册为Bean
public class SpringBootTransactionApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTransactionApplication.class, args);
}
}
@MapperScan("com.imooc.springboottransaction") // 指定MyBatis扫描的包,以便将数据访问接口注册为Bean
public class SpringBootTransactionApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTransactionApplication.class, args);
}
}
扫描数据访问接口所在的包。
添加 MyBatis 映射文件(GoodsMapper.xml、OrderMapper.xml)
编写 GoodsDao 、 OrderDao 对应的映射文件, 首先我们:
通过 application.properties 指定映射文件的位置
# 指定MyBatis配置文件位置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.mapper-locations=classpath:mapper/*.xml
然后在 resources/mapper 目录下,新建 GoodsMapper.xml 文件
该文件就是 goods 表对应的映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应GoodsDao接口 -->
<mapper namespace="com.imooc.springboottransaction.GoodsDao">
<!-- 对应GoodsDao中的selectForUpdate方法 -->
<select id="selectForUpdate" resultMap="resultMapBase" parameterType="java.lang.Long">
select <include refid="sqlBase" /> from goods where id = #{id} for update
</select>
<!-- 对应GoodsDao中的update方法 -->
<update id="update" parameterType="com.imooc.springboottransaction.GoodsDo">
update goods set name=#{name},num=#{num} where id=#{id}
</update>
<!-- 可复用的sql模板 -->
<sql id="sqlBase">
id,name,num
</sql>
<!-- 保存SQL语句查询结果与实体类属性的映射 -->
<resultMap id="resultMapBase" type="com.imooc.springboottransaction.GoodsDo">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="num" property="num" />
</resultMap>
</mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应GoodsDao接口 -->
<mapper namespace="com.imooc.springboottransaction.GoodsDao">
<!-- 对应GoodsDao中的selectForUpdate方法 -->
<select id="selectForUpdate" resultMap="resultMapBase" parameterType="java.lang.Long">
select <include refid="sqlBase" /> from goods where id = #{id} for update
</select>
<!-- 对应GoodsDao中的update方法 -->
<update id="update" parameterType="com.imooc.springboottransaction.GoodsDo">
update goods set name=#{name},num=#{num} where id=#{id}
</update>
<!-- 可复用的sql模板 -->
<sql id="sqlBase">
id,name,num
</sql>
<!-- 保存SQL语句查询结果与实体类属性的映射 -->
<resultMap id="resultMapBase" type="com.imooc.springboottransaction.GoodsDo">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="num" property="num" />
</resultMap>
</mapper>
同样在 resources/mapper 目录下,新建 OrderMapper.xml 文件
该文件是 order 表对应的映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应OrderDao接口 -->
<mapper namespace="com.imooc.springboottransaction.OrderDao">
<!-- 对应OrderDao中的insert方法 -->
<insert id="insert" parameterType="com.imooc.springboottransaction.OrderDo">
insert into `order` (goods_id,count) values (#{goodsId},#{count})
</insert>
</mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应OrderDao接口 -->
<mapper namespace="com.imooc.springboottransaction.OrderDao">
<!-- 对应OrderDao中的insert方法 -->
<insert id="insert" parameterType="com.imooc.springboottransaction.OrderDo">
insert into `order` (goods_id,count) values (#{goodsId},#{count})
</insert>
</mapper>
编写服务方法(OrderService)
下单这个操作,可以封装为一个服务方法,不管是手机端下单还是电脑端下单都可以调用。
新建订单服务类 OrderService ,并在其中实现下单方法 createOrder ,代码如下:
/**
* 订单服务类
*/
@Service // 注册为服务类
public class OrderService {
@Autowired
private GoodsDao goodsDao;
@Autowired
private OrderDao orderDao;
/**
* 下单
*
* @param goodsId 购买商品id
* @param count 购买商品数量
* @return 生成订单数
*/
@Transactional // 实现事务
public int createOrder(Long goodsId, Long count) {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
}
* 订单服务类
*/
@Service // 注册为服务类
public class OrderService {
@Autowired
private GoodsDao goodsDao;
@Autowired
private OrderDao orderDao;
/**
* 下单
*
* @param goodsId 购买商品id
* @param count 购买商品数量
* @return 生成订单数
*/
@Transactional // 实现事务
public int createOrder(Long goodsId, Long count) {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
}
createOrder 方法上添加了 @Transactional 注解
该注解为 createOrder 方法开启了事务,当方法结束时提交事务
这样,保证了 createOrder 内方法全部执行成功,或者全部失败。
测试
构造测试数据
在数据库中构造一条测试数据如下:
测试数据
正常测试
编写测试方法发起测试:
/**
* 订单测试
*/
@SpringBootTest
class OrderTest {
@Autowired
private OrderService orderService;
/**
* 新增一个商品
*/
@Test
void testCreateOrder() {
// 购买id为1的商品1份
int affectRows = orderService.createOrder(1L, 1L);
assertEquals(1, affectRows);
}
}
* 订单测试
*/
@SpringBootTest
class OrderTest {
@Autowired
private OrderService orderService;
/**
* 新增一个商品
*/
@Test
void testCreateOrder() {
// 购买id为1的商品1份
int affectRows = orderService.createOrder(1L, 1L);
assertEquals(1, affectRows);
}
}
运行测试方法后,手机的库存变为 19 ,且生成一条订单记录,测试通过,具体结果如下:
正常测试结果
模拟异常测试
修改下单方法,在扣减库存后抛出异常,看看事务能否回滚到修改全部未发生的状态。
为了便于测试我们将库存重新设为 20 ,然后将下单方法修改如下:
@Transactional // 实现事务
public int createOrder(Long goodsId, Long count) {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
// 模拟异常
int a=1/0;
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
public int createOrder(Long goodsId, Long count) {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
// 模拟异常
int a=1/0;
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
运行测试方法后,抛出异常,查看数据库发现,库存还是 20
说明 goodsDao.update(goods); 的修改没有提交到数据库,具体结果如下:
模拟异常测试结果
Spring 事务在一些情况下不能生效
抛出检查型异常时事务失效(默认只对非受检查异常回滚)
异常类型
Exception 受检查的异常
在程序中,必须使用 try…catch 进行处理
遇到这种异常不处理,编译器会报错。
例如, IOException 。
@Transactional失效
RuntimeException 非受检查的异常
可以不使用 try…catch 进行处理。
例如,常见的 NullPointerException 。
@Transactional有效
【错误观念】潜意识中,只要发生异常,事务就应该回滚
实际上使用 @Transactional 时,默认只对非受检查异常回滚。
如果想实现只要抛出异常就回滚,可通过添加注解 @Transactional(rollbackFor=Exception.class) 实现
@Transactional(rollbackFor = Exception.class) // 抛出异常即回滚
public int createOrder(Long goodsId, Long count) throws Exception {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
if (count > goods.getNum()) {
throw new Exception();
}
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
public int createOrder(Long goodsId, Long count) throws Exception {
// 锁定商品库存
GoodsDo goods = goodsDao.selectForUpdate(goodsId);
// 扣减库存
Long newNum = goods.getNum() - count;
goods.setNum(newNum);
goodsDao.update(goods);
if (count > goods.getNum()) {
throw new Exception();
}
// 生成订单
OrderDo order = new OrderDo();
order.setGoodsId(goodsId);
order.setCount(count);
int affectRows = orderDao.insert(order);
return affectRows;
}
一个事务方法调用另一个事务方法时失效
如果在同一个类中,一个事务方法调用另一个事务方法,可能会导致被调用的事务方法的事务失效!
这是因为 Spring 的声明式事务使用了代理
说明
Spring Boot 中的事务使用非常简单,是因为进行了高度的封装。
正是由于封装的很彻底,所以我们一般接触不到其具体原理和实现方式
这就需要我们注意一些事务可能失效的情况,避免因事务失效带来风险和损失。
Spring Boot 多数据源与分布式事务
为什么需要多数据源与分布式事务?
一个项目中使用多个数据源的需求,在日常工作中时常会遇到。
以商城系统为例
MySQL
有一个 MySQL 的数据库负责存储交易数据。
SQL Server
公司还有一套 ERP 企业信息化管理系统,要求订单信息同步录入 ERP 数据库,便于公司统一管理
而该 ERP 系统采用的数据库为 SQL Server 。
此时,就可以在 Spring Boot 项目中配置多个数据源。
另外,使用多数据源后,需要采用分布式事务来保持数据的完整性。
配置多个数据源时,通过配置类,逐一配置数据源组件及其参数。
配置 MyBatis 时,手工配置 MyBatis 相关组件,并指定相应扫描的 DAO 类及映射文件。
使用分布式事务时,使用支持分布式事务的数据源组件,并配置分布式事务管理器。
配置 MyBatis 时,手工配置 MyBatis 相关组件,并指定相应扫描的 DAO 类及映射文件。
使用分布式事务时,使用支持分布式事务的数据源组件,并配置分布式事务管理器。
实例场景
接下来,开始实现 Spring Boot 后端项目,数据持久层采用 MyBatis 框架,同时访问两个数据源。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-multidb
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入热部署依赖、 Web 依赖、数据库访问相关依赖及测试相关依赖
具体如下POM文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- SQL Server驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- SQL Server驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<!-- 集成MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
数据源配置
由于要同时访问两个数据库,所以需要在配置文件中添加两个数据源的配置信息。
注意配置多数据源时, url 配置需要使用 spring.datasource.db1.jdbc-url=xxx 的形式。
# 数据源1 MySQL
spring.datasource.db1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.jdbc-url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.db1.username=root
spring.datasource.db1.password=Easy@0122
# 数据源2 SQL Server
spring.datasource.db2.driverClassName = com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.db2.jdbc-url =jdbc:sqlserver://127.0.0.1:1433;DatabaseName=erpshop
spring.datasource.db2.username =sa
spring.datasource.db2.password =Easy@0122
spring.datasource.db1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.jdbc-url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.db1.username=root
spring.datasource.db1.password=Easy@0122
# 数据源2 SQL Server
spring.datasource.db2.driverClassName = com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.db2.jdbc-url =jdbc:sqlserver://127.0.0.1:1433;DatabaseName=erpshop
spring.datasource.db2.username =sa
spring.datasource.db2.password =Easy@0122
注册数据源组件
多个数据源的情况下, 需要通过配置类,将数据源注册为组件放入 Spring 容器中。
//数据源配置类
@Configuration//标注为配置类
public class DataSourceConfig {
//数据源1
@Bean//返回值注册为组件
@ConfigurationProperties("spring.datasource.db1")//使用其作为前缀的配置
public DataSource db1() {
return DataSourceBuilder.create().build();
}
//数据源2
@Bean//返回值注册为组件
@ConfigurationProperties("spring.datasource.db2")//使用其作为前缀的配置
public DataSource db2() {
return DataSourceBuilder.create().build();
}
}
@Configuration//标注为配置类
public class DataSourceConfig {
//数据源1
@Bean//返回值注册为组件
@ConfigurationProperties("spring.datasource.db1")//使用其作为前缀的配置
public DataSource db1() {
return DataSourceBuilder.create().build();
}
//数据源2
@Bean//返回值注册为组件
@ConfigurationProperties("spring.datasource.db2")//使用其作为前缀的配置
public DataSource db2() {
return DataSourceBuilder.create().build();
}
}
通过这个配置类, Spring 容器中就有两个数据源组件,这两个组件分别采用 spring.datasource.db1 和 spring.datasource.db2 开头的配置信息。
所以通过这两个组件,就能分别操作 MySQL 数据源 1 和 SQL Sever 数据源 2 。
MyBatis 配置
多数据源情况下, MyBatis 中的关键组件 SqlSessionFactory 和 SqlSessionTemplate 也需要单独配置,
需要为两个数据源分别配置一套组件。
目的:关联两个不同【数据源】 – 【DAO 数据访问接口】 – 【映射文件】三者的对应关系就建立起来了。
数据源1 MyBatis配置
【DAO 数据访问接口】通过配置类com.imooc.springbootmultidb.mapper1 包中的 DAO 数据访问接口会自动调用 sqlSessionTemplate1 组件,实现具体数据库操作
【数据源】而 sqlSessionTemplate1 操作的数据源已经通过配置类设置为 db1 。
【映射文件】同时, DAO 数据访问接口对应的映射文件已经指定到 classpath:mapper1/ 目录去寻找。
代码
//数据源1 MyBatis配置
@Configuration
@MapperScan(value = "com.imooc.springbootmultidb.mapper1", sqlSessionFactoryRef = "sqlSessionFactory1")
public class Db1MyBatisConfig {
@Autowired // 自动装配
@Qualifier("db1") // 指定注入名为db1的组件
private DataSource db1;
@Bean
public SqlSessionFactory sqlSessionFactory1() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db1);// sqlSessionFactory1使用的数据源为db1
sqlSessionFactoryBean
.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper1/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory1());// sqlSessionTemplate1使用的数据源也是关联到db1
}
}
@Configuration
@MapperScan(value = "com.imooc.springbootmultidb.mapper1", sqlSessionFactoryRef = "sqlSessionFactory1")
public class Db1MyBatisConfig {
@Autowired // 自动装配
@Qualifier("db1") // 指定注入名为db1的组件
private DataSource db1;
@Bean
public SqlSessionFactory sqlSessionFactory1() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db1);// sqlSessionFactory1使用的数据源为db1
sqlSessionFactoryBean
.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper1/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory1());// sqlSessionTemplate1使用的数据源也是关联到db1
}
}
数据源2 MyBatis配置
数据源 2 的配置方法是一样的
通过上面的配置类
【DAO 数据访问接口】通过上面的配置类com.imooc.springbootmultidb.mapper2 包中的 DAO 数据访问接口会自动调用 sqlSessionTemplate2 组件,实现具体数据库操作
【数据源】而 sqlSessionTemplate2 操作的数据源已经通过配置类设置为db2 ,
【映射文件】同时, DAO 数据访问接口对应的映射文件已经指定到 classpath:mapper2/ 目录去寻找。
代码
//数据源2 MyBatis配置
@Configuration
@MapperScan(value = "com.imooc.springbootmultidb.mapper2", sqlSessionFactoryRef = "sqlSessionFactory2")
public class Db2MyBatisConfig {
@Autowired // 自动装配
@Qualifier("db2") // 指定注入名为db1的组件
private DataSource db2;
@Bean
public SqlSessionFactory sqlSessionFactory2() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db2);// sqlSessionFactory2使用的数据源为db2
sqlSessionFactoryBean
.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper2/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate2() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory2());// sqlSessionTemplate2使用的数据源也是关联到db2
}
}
@Configuration
@MapperScan(value = "com.imooc.springbootmultidb.mapper2", sqlSessionFactoryRef = "sqlSessionFactory2")
public class Db2MyBatisConfig {
@Autowired // 自动装配
@Qualifier("db2") // 指定注入名为db1的组件
private DataSource db2;
@Bean
public SqlSessionFactory sqlSessionFactory2() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db2);// sqlSessionFactory2使用的数据源为db2
sqlSessionFactoryBean
.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper2/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate2() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory2());// sqlSessionTemplate2使用的数据源也是关联到db2
}
}
数据对象与数据访问接口实现
数据访问接口的位置已经在配置类指定
创建 OrderDao
首先在 com.imooc.springbootmultidb.mapper1 创建 OrderDao
操作的是数据源 1 中的 order 表
//数据访问接口
@Repository
public interface OrderDao {
public int insert(OrderDo order);
}
@Repository
public interface OrderDao {
public int insert(OrderDo order);
}
创建 ErpOrderDao
在 com.imooc.springbootmultidb.mapper2 创建 ErpOrderDao
操作的是数据源 2 中的 erporder 表。
//数据访问接口
@Repository
public interface ErpOrderDao {
public int insert(ErpOrderDo erpOrder);
}
@Repository
public interface ErpOrderDao {
public int insert(ErpOrderDo erpOrder);
}
数据对象ErpOrderDo
//ERP订单数据类
public class ErpOrderDo {
//订单id
private Long id;
//商城系统订单id
private Long outId;
//商品id
private Long goodsId;
//购买数量
private Long count;
// 省略 get set
}
public class ErpOrderDo {
//订单id
private Long id;
//商城系统订单id
private Long outId;
//商品id
private Long goodsId;
//购买数量
private Long count;
// 省略 get set
}
数据对象OrderDo
//订单数据类
public class OrderDo {
//订单id
private Long id;
//商品id
private Long goodsId;
//购买数量
private Long count;
// 省略 get set
}
public class OrderDo {
//订单id
private Long id;
//商品id
private Long goodsId;
//购买数量
private Long count;
// 省略 get set
}
编写映射文件
分别针对 OrderDao 、 ErpOrderDao 编写对应的映射文件,然后按照配置类指定的位置
两个文件分别放到 resources/mapper1 和 resources/mapper2 目录下。
resources/mapper1
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应OrderDao接口 -->
<mapper namespace="com.imooc.springbootmultidb.mapper1.OrderDao">
<!-- 对应OrderDao中的insert方法 -->
<insert id="insert"
parameterType="com.imooc.springbootmultidb.mapper1.OrderDo"
useGeneratedKeys="true" keyProperty="id">
insert into `order`
(goods_id,count) values (#{goodsId},#{count})
</insert>
</mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应OrderDao接口 -->
<mapper namespace="com.imooc.springbootmultidb.mapper1.OrderDao">
<!-- 对应OrderDao中的insert方法 -->
<insert id="insert"
parameterType="com.imooc.springbootmultidb.mapper1.OrderDo"
useGeneratedKeys="true" keyProperty="id">
insert into `order`
(goods_id,count) values (#{goodsId},#{count})
</insert>
</mapper>
resources/mapper2
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应ErpOrderDao接口 -->
<mapper
namespace="com.imooc.springbootmultidb.mapper2.ErpOrderDao">
<!-- 对应ErpOrderDao中的insert方法 -->
<insert id="insert"
parameterType="com.imooc.springbootmultidb.mapper2.ErpOrderDo">
insert into erp_order (out_id,goods_id,count) values
(#{outId},#{goodsId},#{count})
</insert>
</mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 本映射文件对应ErpOrderDao接口 -->
<mapper
namespace="com.imooc.springbootmultidb.mapper2.ErpOrderDao">
<!-- 对应ErpOrderDao中的insert方法 -->
<insert id="insert"
parameterType="com.imooc.springbootmultidb.mapper2.ErpOrderDo">
insert into erp_order (out_id,goods_id,count) values
(#{outId},#{goodsId},#{count})
</insert>
</mapper>
多数据源测试
数据操作接口与对应的映射文件均已编写完毕,现在可以通过测试类进行多数据源测试了,我们在测试类中同时向两个库插入记录。
代码
// 多数据源测试
@SpringBootTest
class MultidbTest {
@Autowired
private OrderDao orderDao;// 对应数据源1
@Autowired
private ErpOrderDao erpOrderDao;// 对应数据源2
//插入测试
@Test
void testInsert() {
// 数据源1插入数据
OrderDo order = new OrderDo();
order.setCount(1L);
order.setGoodsId(1L);
int affectRows1 = orderDao.insert(order);
// 数据源2插入数据
ErpOrderDo erpOrder = new ErpOrderDo();
erpOrder.setCount(order.getCount());
erpOrder.setGoodsId(order.getGoodsId());
erpOrder.setOutId(order.getId());
int affectRows2 = erpOrderDao.insert(erpOrder);
assertEquals(1, affectRows1);
assertEquals(1, affectRows2);
}
}
@SpringBootTest
class MultidbTest {
@Autowired
private OrderDao orderDao;// 对应数据源1
@Autowired
private ErpOrderDao erpOrderDao;// 对应数据源2
//插入测试
@Test
void testInsert() {
// 数据源1插入数据
OrderDo order = new OrderDo();
order.setCount(1L);
order.setGoodsId(1L);
int affectRows1 = orderDao.insert(order);
// 数据源2插入数据
ErpOrderDo erpOrder = new ErpOrderDo();
erpOrder.setCount(order.getCount());
erpOrder.setGoodsId(order.getGoodsId());
erpOrder.setOutId(order.getId());
int affectRows2 = erpOrderDao.insert(erpOrder);
assertEquals(1, affectRows1);
assertEquals(1, affectRows2);
}
}
运行测试方法后,两个数据库表中均新增数据成功,这样就成功的使用 Spring Boot 同时操作了两个数据源。
分布式事务
分布式事务是什么?
使用多数据源后,需要采用分布式事务来保持数据的完整性。
采用多数据源之后,事务的实现方式也随之发生变化。
当某个数据源操作出现异常时,该数据源和其他数据源的事务都需要回滚。
这种涉及多个数据源的事务,称为分布式事务
Atomikos是什么?
是一个开源的事务管理器,支持分布式事务。
引入分布式事务依赖
在 pom.xml 引入 Atomikos 事务管理器相关的依赖项,
POM文件
<!--分布式事务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
更换数据源组件
需要将默认的数据源更换为支持分布式事务的数据源
MySQL 对应的数据源为 MysqlXADataSource
SQL Server 对应的数据源为 SQLServerXADataSource 。
数据源配置类
/**
* 数据源配置类
*/
@Configuration // 标注为配置类
public class DataSourceConfig {
// 注入数据源1配置项
@Value("${spring.datasource.db1.jdbc-url}")
private String db1_url;
@Value("${spring.datasource.db1.username}")
private String db1_username;
@Value("${spring.datasource.db1.password}")
private String db1_password;
// 注入数据源2配置项
@Value("${spring.datasource.db2.jdbc-url}")
private String db2_url;
@Value("${spring.datasource.db2.username}")
private String db2_username;
@Value("${spring.datasource.db2.password}")
private String db2_password;
/**
* 数据源1
*/
@Bean // 返回值注册为组件
public DataSource db1() throws SQLException {
MysqlXADataSource dataSource = new MysqlXADataSource();
dataSource.setUrl(db1_url);
dataSource.setUser(db1_username);
dataSource.setPassword(db1_password);
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(dataSource);
atomikosDataSourceBean.setUniqueResourceName("db1");
return atomikosDataSourceBean;
}
/**
* 数据源2
*/
@Bean // 返回值注册为组件
public DataSource db2() {
SQLServerXADataSource dataSource = new SQLServerXADataSource();
dataSource.setURL(db2_url);
dataSource.setUser(db2_username);
dataSource.setPassword(db2_password);
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(dataSource);
atomikosDataSourceBean.setUniqueResourceName("db2");
return atomikosDataSourceBean;
}
}
* 数据源配置类
*/
@Configuration // 标注为配置类
public class DataSourceConfig {
// 注入数据源1配置项
@Value("${spring.datasource.db1.jdbc-url}")
private String db1_url;
@Value("${spring.datasource.db1.username}")
private String db1_username;
@Value("${spring.datasource.db1.password}")
private String db1_password;
// 注入数据源2配置项
@Value("${spring.datasource.db2.jdbc-url}")
private String db2_url;
@Value("${spring.datasource.db2.username}")
private String db2_username;
@Value("${spring.datasource.db2.password}")
private String db2_password;
/**
* 数据源1
*/
@Bean // 返回值注册为组件
public DataSource db1() throws SQLException {
MysqlXADataSource dataSource = new MysqlXADataSource();
dataSource.setUrl(db1_url);
dataSource.setUser(db1_username);
dataSource.setPassword(db1_password);
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(dataSource);
atomikosDataSourceBean.setUniqueResourceName("db1");
return atomikosDataSourceBean;
}
/**
* 数据源2
*/
@Bean // 返回值注册为组件
public DataSource db2() {
SQLServerXADataSource dataSource = new SQLServerXADataSource();
dataSource.setURL(db2_url);
dataSource.setUser(db2_username);
dataSource.setPassword(db2_password);
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(dataSource);
atomikosDataSourceBean.setUniqueResourceName("db2");
return atomikosDataSourceBean;
}
}
添加分布式事务管理器组件
继续修改 DataSourceConfig 类,在其中配置分布式事务管理器组件。
当项目中使用事务时,会通过配置的分布式事务管理器管理分布式事务操作。
//分布式事务管理器
@Bean(name = "jtaTransactionManager")
public JtaTransactionManager jtaTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
@Bean(name = "jtaTransactionManager")
public JtaTransactionManager jtaTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
测试分布式事务
在测试方法上添加 @Transactional 开启事务,然后在两个数据源操作中间模拟抛出异常。
//插入测试
@Test
@Transactional // 开启事务
void testInsert() {
// 数据源1插入数据
OrderDo order = new OrderDo();
order.setCount(1L);
order.setGoodsId(1L);
int affectRows1 = orderDao.insert(order);
// 模拟抛出异常
int a = 1 / 0;
// 数据源2插入数据
ErpOrderDo erpOrder = new ErpOrderDo();
erpOrder.setCount(order.getCount());
erpOrder.setGoodsId(order.getGoodsId());
erpOrder.setOutId(order.getId());
int affectRows2 = erpOrderDao.insert(erpOrder);
assertEquals(1, affectRows1);
assertEquals(1, affectRows2);
}
@Test
@Transactional // 开启事务
void testInsert() {
// 数据源1插入数据
OrderDo order = new OrderDo();
order.setCount(1L);
order.setGoodsId(1L);
int affectRows1 = orderDao.insert(order);
// 模拟抛出异常
int a = 1 / 0;
// 数据源2插入数据
ErpOrderDo erpOrder = new ErpOrderDo();
erpOrder.setCount(order.getCount());
erpOrder.setGoodsId(order.getGoodsId());
erpOrder.setOutId(order.getId());
int affectRows2 = erpOrderDao.insert(erpOrder);
assertEquals(1, affectRows1);
assertEquals(1, affectRows2);
}
此时运行测试类,可以发现数据源 1 的事务已回滚,验证成功!
如果运行测试类报错 master..xp_sqljdbc_xa_init_ex 相关信息,是 SQL Server 默认配置不支持分布式事务问题
Spring Boot 运行管理
Spring Boot 日志管理
日志是重要的,不可或缺的
谁能保证开发的软件系统没有问题?恐怕任何一个有经验的程序员都不敢承诺吧!
在软件的设计、开发阶段,大家都是尽心尽力去做好各项工作,期望能有一个满意的效果。
但是一个投入生产环境、拥有众多用户的软件系统必然是一个复杂的系统工程,不经历现实的检验,没有人能准确地知道它到底会不会有问题。
日志是软件系统出现故障时,分析问题的主要依据。
就像飞机的黑匣子,平时感觉毫不起眼,到了关键时刻必须要依靠它!
日志系统对生产环境项目来说是不可或缺的,大家可以选择使用 Spring Boot 集成一种自己用起来顺手的日志系统。
Spring Boot 日志管理
Spring Boot 项目可以使用简单的几个配置,实现日志的打印,并设置相应的级别、日志文件等信息。
SpringBoot与默认的日志配置(INFO+Logback)
Spring Boot 默认已经集成了日志功能
Spring Boot 日志默认级别是 INFO
使用的是 logback 开源日志系统
Spring Boot默认使用LogBack日志系统,如果不需要更改为其他日志系统如Log4j2等,则无需多余的配置,LogBack默认将日志打印到控制台上。
如果要使用LogBack,原则上是需要添加dependency依赖的
但是因为新建的Spring Boot项目一般都会引用spring-boot-starter或者spring-boot-starter-web,
而这两个起步依赖中都已经包含了对于spring-boot-starter-logging的依赖,所以,无需额外添加依赖。
验证方式
新建一个项目,Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-log。生成项目后导入 Eclipse 开发环境
运行启动类,可以清楚地看到控制台打印的日志信息。
下图也输出了几条 INFO 级别的日志
Spring Boot 项目启动时控制台输出的内容
输出内容含义
日期时间:精确到毫秒。
日志级别:打印 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 等级别日志信息。
进程 ID:当前项目进程 ID 。
分隔符:--- 是分隔符,分隔符后面代表具体的日志内容。
线程名:方括号中间的内容表示线程名称。
类名:当前日志打印所属的类名称。
日志内容:开发人员设定的日志具体内容。
日志级别控制(logging.level.root)
修改配置文件设置日志级别
想指定打印的日志的级别,可以通过配置文件来设置。
# 设置日志级别
logging.level.root=WARN
logging.level.root=WARN
上面的配置表示项目日志的记录级别为 WARN
所以会打印 WARN 及优先级更高的 ERROR 级别的日志。
测试验证打印日志的情况
此时编写一个测试类(LogTest),看看具体打印日志的情况。
@SpringBootTest
class LogTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Test
void testPrintLog() {
logger.trace("trace log");
logger.debug("debug log");
logger.info("info log");
logger.warn("warn log");
logger.error("error log");
}
}
class LogTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Test
void testPrintLog() {
logger.trace("trace log");
logger.debug("debug log");
logger.info("info log");
logger.warn("warn log");
logger.error("error log");
}
}
控制台打印内容验证
运行测试类,控制台打印内容如下,说明我们指定的日志级别生效了。
控制台打印指定级别日志
实现更细维度的日志级别控制
Tips: logging.level.root=WARN 中的 root
可以改为指定包名或类名
表示设置指定包或者类的日志级别。
指定日志文件(logging.file 、 logging.path)
修改配置文件指定日志文件
控制台日志保存的内容十分有限
大多数情况下,需要将日志写入文件,便于追溯。
可以通过配置文件指定日志文件,
# 设置日志文件
logging.file=C:\\logs\\spring-boot-log.log
logging.file=C:\\logs\\spring-boot-log.log
以上配置会将日志打印到 C:\\logs\\spring-boot-log.log 文件中。
修改配置文件指定日志文件输出的目录
也可以指定日志文件输出的目录
Spring Boot 项目会在指定输出目录下新建 spring.log 文件,并在文件中写入日志。
# 设置日志目录
logging.path=C:\\logs
logging.path=C:\\logs
Tips:如果同时配置了 logging.file 和 logging.path ,则只有 logging.file 生效。
使用插件简化日志代码(Lombok)
在上面的示例中,如果要打印日志,需要添加一行代码 private Logger logger = LoggerFactory.getLogger(this.getClass()); 还是比较麻烦的。
可安装 lombok 插件,使用一个注解代替这行代码。
lombok 插件
简化日志模板代码
自动生成常用的 getter /setter/toString 等模板代码
步骤
下载 lombok 插件
安装 lombok 插件
引入 lombok 依赖
使用注解输出日志
自定义日志配置
修改不同的日志系统
如 logback 、log4j 。
如果想要对日志的方方面面进行设定,也可以快速地集成常见的日志系统
Spring Boot 也支持自定义日志配置,可以直接采用指定日志系统的配置文件
在配置文件中指定 logback 配置文件
以 logback 为例,可以直接在 application.properties 文件中指定 logback 配置文件。
# 指定logback配置文件,位于resources目录下
logging.config=classpath:logback-spring.xml
logging.config=classpath:logback-spring.xml
指定logback配置文件
Tips:使用 logback 日志系统后,日志级别与日志文件等信息都可以使用 logback-spring.xml 文件设置
不再需要从 properties 文件中设置了。
logback-spring.xml 文件示例
在生产环境,希望指定日志保存的位置,另外日志不能无限制一直保存,一般情况下保存最近 30 天左右的日志即可。
这些都可以在 logback-spring.xml 文件中指定,此处给出一个完整实例供大家参考。
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback 配置 -->
<configuration>
<!-- 输出到控制台 -->
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期;%thread表示线程名;%-5level:左对齐并固定显示5个字符;%msg:日志消息;%n:换行符; -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
%msg%n</pattern>
</encoder>
</appender>
<!-- 输出到文件 -->
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在打印的日志文件 -->
<File>C:/logs/spring-boot-log.log</File>
<encoder>
<!--格式化输出:%d表示日期;%thread表示线程名;%-5level:左对齐并固定显示5个字符;%msg:日志消息;%n:换行符; -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
%msg%n
</pattern>
</encoder>
<!-- 日志文件的滚动策略 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志归档 -->
<fileNamePattern>C:/logs/spring-boot-log-%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- 保留30天日志 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 指定日志输出的级别 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
<!-- logback 配置 -->
<configuration>
<!-- 输出到控制台 -->
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期;%thread表示线程名;%-5level:左对齐并固定显示5个字符;%msg:日志消息;%n:换行符; -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
%msg%n</pattern>
</encoder>
</appender>
<!-- 输出到文件 -->
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在打印的日志文件 -->
<File>C:/logs/spring-boot-log.log</File>
<encoder>
<!--格式化输出:%d表示日期;%thread表示线程名;%-5level:左对齐并固定显示5个字符;%msg:日志消息;%n:换行符; -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
%msg%n
</pattern>
</encoder>
<!-- 日志文件的滚动策略 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志归档 -->
<fileNamePattern>C:/logs/spring-boot-log-%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- 保留30天日志 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 指定日志输出的级别 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
Spring Boot 异常处理
背景说明
程序中出现异常是普遍现象, Java 程序员想必早已习惯,根据控制台输出的异常信息,分析异常产生的原因,然后进行针对性处理的过程。
Spring Boot 项目中,数据持久层、服务层到控制器层都可能抛出异常。
如果在各层都进行异常处理,程序代码会显得支离破碎,难以理解。
实际上,异常可以从内层向外层不断抛出,最后在控制器层进行统一处理。
不管项目有多少层次,所有异常都可以向外抛出,直到控制器层进行集中处理。
在开发 Spring Boot 项目时,需要根据项目的实际情况,定义各类异常,并站在全局的角度统一处理异常。
Spring Boot 提供了全局性的异常处理机制,三种情况的异常处理方法
默认情况
Spring Boot 的默认异常处理机制
实际上只能做到提醒开发者 “这个后端接口不存在” 的作用,
作用非常有限
控制器返回视图
对于返回视图的控制器
如果没发生异常就跳转正常页面
如果发生异常可以自定义错误信息页面。
控制器返回 JSON 数据
对于返回 JSON 数据的控制器
最好是定义统一的数据返回格式,便于前端根据返回信息进行正常或者异常情况的处理。
Spring Boot 默认异常处理机制
Spring Boot 开发的 Web 项目具备默认的异常处理机制,无须编写异常处理相关代码,即可提供默认异常机制,下面具体演示下。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-default ,生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖即可。
<!-- web项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Spring Boot 默认异常处理
我们在启动项目, Spring Boot Web 项目默认启动端口为 8080 ,所以直接访问 http://127.0.0.1:8080 ,显示如下:
Spring Boot 默认异常信息提示页面
如上图所示,Spring Boot 默认的异常处理机制生效,当出现异常时会自动转向 /error 路径。
控制器返回视图时的异常处理
在使用模板引擎开发 Spring Boot Web 项目时,控制器会返回视图页面。
使用 Thymeleaf 演示控制器返回视图时的异常处理方式,其他模板引擎处理方式也是相似的。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-controller,
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖。此处使用 Thymeleaf 演示控制器返回视图时的异常处理方式,所以引入 Thymeleaf 依赖。
<!-- web项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- ThymeLeaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- ThymeLeaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
定义异常类
在异常处理之前,应该根据业务场景具体情况,定义一系列的异常类
习惯性的还会为各种异常分配错误码,如下图为支付宝开放平台的公共错误码信息。
支付宝开放平台错误码
本节为了演示,简单的定义 2 个异常类,包含错误码及错误提示信息。
/**
* 自定义异常
*/
public class BaseException extends Exception {
/**
* 错误码
*/
private int code;
/**
* 错误提示信息
*/
private String msg;
public BaseException(int code, String msg) {
super();
this.code = code;
this.msg = msg;
}
// 省略get set
}
* 自定义异常
*/
public class BaseException extends Exception {
/**
* 错误码
*/
private int code;
/**
* 错误提示信息
*/
private String msg;
public BaseException(int code, String msg) {
super();
this.code = code;
this.msg = msg;
}
// 省略get set
}
/**
* 密码错误异常
*/
public class PasswordException extends BaseException {
public PasswordException() {
super(10001, "密码错误");
}
}
* 密码错误异常
*/
public class PasswordException extends BaseException {
public PasswordException() {
super(10001, "密码错误");
}
}
/**
* 验证码错误异常
*/
public class VerificationCodeException extends BaseException {
public VerificationCodeException() {
super(10002, "验证码错误");
}
}
* 验证码错误异常
*/
public class VerificationCodeException extends BaseException {
public VerificationCodeException() {
super(10002, "验证码错误");
}
}
控制器抛出异常
定义控制器 GoodsController ,然后使用注解 @Controller 标注该类,类中方法的返回值即为视图文件名。
在 GoodsController 类定义 4 个方法,分别用于正常访问、抛出密码错误异常、抛出验证码错误异常、抛出未自定义的异常,代码如下。
/**
* 商品控制器
*/
@Controller
public class GoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public String goods() {
return "goods";// 跳转到resource/templates/goods.html页面
}
/**
* 抛出密码错误异常的方法
*/
@RequestMapping("/checkPassword")
public String checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模拟抛出异常,便于测试
}
return "goods";
}
/**
* 抛出验证码错误异常的方法
*/
@RequestMapping("/checkVerification")
public String checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模拟抛出异常,便于测试
}
return "goods";
}
/**
* 抛出未自定义的异常
*/
@RequestMapping("/other")
public String other() throws Exception {
int a = 1 / 0;// 模拟异常
return "goods";
}
}
* 商品控制器
*/
@Controller
public class GoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public String goods() {
return "goods";// 跳转到resource/templates/goods.html页面
}
/**
* 抛出密码错误异常的方法
*/
@RequestMapping("/checkPassword")
public String checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模拟抛出异常,便于测试
}
return "goods";
}
/**
* 抛出验证码错误异常的方法
*/
@RequestMapping("/checkVerification")
public String checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模拟抛出异常,便于测试
}
return "goods";
}
/**
* 抛出未自定义的异常
*/
@RequestMapping("/other")
public String other() throws Exception {
int a = 1 / 0;// 模拟异常
return "goods";
}
}
开发基于 @ControllerAdvice 的全局异常类
@ControllerAdvice 注解标注的类, 可以处理 @Controller 标注的控制器类抛出的异常,然后进行统一处理。
/**
* 控制器异常处理类
*/
@ControllerAdvice(annotations = Controller.class) // 全局异常处理
public class ControllerExceptionHandler {
@ExceptionHandler({ BaseException.class }) // 当发生BaseException类(及其子类)的异常时,进入该方法
public ModelAndView baseExceptionHandler(BaseException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", e.getCode());
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
return mv;
}
@ExceptionHandler({ Exception.class }) // 当发生Exception类的异常时,进入该方法
public ModelAndView exceptionHandler(Exception e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", 99999);// 其他异常统一编码为99999
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
return mv;
}
}
* 控制器异常处理类
*/
@ControllerAdvice(annotations = Controller.class) // 全局异常处理
public class ControllerExceptionHandler {
@ExceptionHandler({ BaseException.class }) // 当发生BaseException类(及其子类)的异常时,进入该方法
public ModelAndView baseExceptionHandler(BaseException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", e.getCode());
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
return mv;
}
@ExceptionHandler({ Exception.class }) // 当发生Exception类的异常时,进入该方法
public ModelAndView exceptionHandler(Exception e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", 99999);// 其他异常统一编码为99999
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
return mv;
}
}
按照 ControllerExceptionHandler 类的处理逻辑,
当发生 BaseException 类型的异常时,会跳转到 myerror.html 页面,并显示相应的错误码和错误信息;
当发生其他类型的异常时,错误码为 99999 ,错误信息为相关的异常信息。
开发前端页面
在 resource/templates 下分别新建 goods.html 和 myerror.html 页面,作为正常访问及发生异常时跳转的视图页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>goods.html页面</title>
</head>
<body>
<div>商品信息页面</div>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>goods.html页面</title>
</head>
<body>
<div>商品信息页面</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>myerror.html页面</title>
</head>
<body>
错误码:
<span th:text="${code}"></span>
错误信息:
<span th:text="${message}"></span>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>myerror.html页面</title>
</head>
<body>
错误码:
<span th:text="${code}"></span>
错误信息:
<span th:text="${message}"></span>
</body>
</html>
测试与验证
启动项目,分别访问控制器中的 4 个方法,结果如下:
验证截图
可见,当控制器方法抛出异常时,会按照全局异常类设定的逻辑统一处理。
控制器返回 JSON 数据时的异常处理
在控制器类上添加 @RestController 注解,控制器方法处理完毕后会返回 JSON 格式的数据。
此时,可以使用 @RestControllerAdvice 注解标注的类 ,来捕获 @RestController 标注的控制器抛出的异常。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-restcontroller,
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、热部署依赖即可。
<!-- web项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
定义异常类
还是使用上文中定义的异常类即可。
统一控制器返回数据格式
这时候,我们就需要思考一个问题了。前端请求后端控制器接口后,怎么区分后端接口是正常返回结果,还是发生了异常?
不论后端接口是正常执行,还是中间发生了异常,最好给前端返回统一的数据格式,便于前端统一分析处理。
OK,此时我们就可以封装后端接口返回的业务逻辑对象 ResultBo ,代码如下:
//后端接口返回的统一业务逻辑对象
public class ResultBo<T> {
//错误码 0表示没有错误(异常) 其他数字代表具体错误码
private int code;
//后端返回消息
private String msg;
// 后端返回的数据
private T data;
//无参数构造函数
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
//带数据data构造函数
public ResultBo(T data) {
this();
this.data = data;
}
//存在异常的构造函数
public ResultBo(Exception ex) {
if (ex instanceof BaseException) {
this.code = ((BaseException) ex).getCode();
this.msg = ex.getMessage();
} else {
this.code = 99999;// 其他未定义异常
this.msg = ex.getMessage();
}
}
// 省略 get set
}
public class ResultBo<T> {
//错误码 0表示没有错误(异常) 其他数字代表具体错误码
private int code;
//后端返回消息
private String msg;
// 后端返回的数据
private T data;
//无参数构造函数
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
//带数据data构造函数
public ResultBo(T data) {
this();
this.data = data;
}
//存在异常的构造函数
public ResultBo(Exception ex) {
if (ex instanceof BaseException) {
this.code = ((BaseException) ex).getCode();
this.msg = ex.getMessage();
} else {
this.code = 99999;// 其他未定义异常
this.msg = ex.getMessage();
}
}
// 省略 get set
}
控制器抛出异常
定义控制器 RestGoodsController ,并使用 @RestController 注解标注。
在其中定义 4 个方法,然后分别用于正常访问、抛出密码错误异常、抛出验证码错误异常,以及抛出不属于自定义异常类的异常。
//Rest商品控制器
@RestController
public class RestGoodsController {
//正常方法
@RequestMapping("/goods")
public ResultBo goods() {
return new ResultBo<>(new ArrayList());// 正常情况下应该返回商品列表
}
//抛出密码错误异常的方法
@RequestMapping("/checkPassword")
public ResultBo checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模拟抛出异常,便于测试
}
return new ResultBo<>(true);// 正常情况下应该返回检查密码的结果true或false
}
//抛出验证码错误异常的方法
@RequestMapping("/checkVerification")
public ResultBo checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模拟抛出异常,便于测试
}
return new ResultBo<>(true);// 正常情况下应该返回检查验证码的结果true或false
}
//抛出未自定义的异常
@RequestMapping("/other")
public ResultBo other() throws Exception {
int a = 1 / 0;// 模拟异常
return new ResultBo<>(true);
}
}
@RestController
public class RestGoodsController {
//正常方法
@RequestMapping("/goods")
public ResultBo goods() {
return new ResultBo<>(new ArrayList());// 正常情况下应该返回商品列表
}
//抛出密码错误异常的方法
@RequestMapping("/checkPassword")
public ResultBo checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模拟抛出异常,便于测试
}
return new ResultBo<>(true);// 正常情况下应该返回检查密码的结果true或false
}
//抛出验证码错误异常的方法
@RequestMapping("/checkVerification")
public ResultBo checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模拟抛出异常,便于测试
}
return new ResultBo<>(true);// 正常情况下应该返回检查验证码的结果true或false
}
//抛出未自定义的异常
@RequestMapping("/other")
public ResultBo other() throws Exception {
int a = 1 / 0;// 模拟异常
return new ResultBo<>(true);
}
}
开发基于 @RestControllerAdvice 的全局异常类
@RestControllerAdvice 注解标注的类可以处理 RestController 控制器类抛出的异常,然后进行统一处理。
//Rest控制器异常处理类
@RestControllerAdvice(annotations = RestController.class) // 全局异常处理
public class RestControllerExceptionHandler {
//处理BaseException类(及其子类)的异常
@ExceptionHandler({ BaseException.class })
public ResultBo baseExceptionHandler(BaseException e) {
return new ResultBo(e);
}
//处理Exception类的异常
@ExceptionHandler({ Exception.class })
public ResultBo exceptionHandler(Exception e) {
return new ResultBo(e);
}
}
@RestControllerAdvice(annotations = RestController.class) // 全局异常处理
public class RestControllerExceptionHandler {
//处理BaseException类(及其子类)的异常
@ExceptionHandler({ BaseException.class })
public ResultBo baseExceptionHandler(BaseException e) {
return new ResultBo(e);
}
//处理Exception类的异常
@ExceptionHandler({ Exception.class })
public ResultBo exceptionHandler(Exception e) {
return new ResultBo(e);
}
}
测试验证
启动项目,分别尝试访问控制器中的 4 个接口,结果如下。
验证截图
Spring Boot 定时任务
定时任务介绍
定时任务绝对是实际项目中的刚需。
指定时间间隔,或者是指定时间节点,按设定的任务进行某种操作,这就是定时任务了。
在 Spring Boot 中实现定时任务简单而灵活
Spring Boot 可以利用一个简单的注解,快速实现定时任务的功能。
使用 @Scheduled 注解时,完全被这种开箱即用型的简洁震撼了,不能更加简洁。
如果感觉 Spring Task 提供的定时任务机制还不足以满足需求,Spring Boot 还可以方便地集成 Quartz 框架来帮忙。
开箱即用满足不了,还可以即插即用,确实够人性化的。
定时任务的使用场景
监控一个重点服务的运行状态,可以每隔 1 分钟调用下该服务的心跳接口,调用失败时即发出告警信息;
每天凌晨时,将所有商品的库存置满,以免早上忘记添加库存影响销售;
在每个周六的某个时段,进行打折促销。
Spring Task 定时任务
SpringTask是什么?
Spring Boot 内置的定时任务模块
可以满足大部分的定时任务场景需求。
通过为方法添加一个简单的注解,即可按设定的规则,定时执行该方法。
Spring Boot 中使用 Spring Task 的具体方法
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-task
生成项目后导入 Eclipse 开发环境。
添加 @EnableScheduling 注解开启定时任务
在启动类上添加 @EnableScheduling 注解,开启定时任务功能。
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class SpringBootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTaskApplication.class, args);
}
}
@EnableScheduling // 开启定时任务
public class SpringBootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTaskApplication.class, args);
}
}
通过@Component+@Scheduled注解设定定时任务
新建 MySpringTask 任务类
添加 @Component 注解注册 Spring 组件,定时任务方法需要在 Spring 组件类才能生效。
注意,类中方法添加了 @Scheduled 注解,所以会按照 @Scheduled 注解参数指定的规则定时执行。
//任务类
@Component
public class MySpringTask {
//每2秒执行1次
@Scheduled(fixedRate = 2000)
public void fixedRateMethod() throws InterruptedException {
System.out.println("fixedRateMethod:" + new Date());
Thread.sleep(1000);
}
}
@Component
public class MySpringTask {
//每2秒执行1次
@Scheduled(fixedRate = 2000)
public void fixedRateMethod() throws InterruptedException {
System.out.println("fixedRateMethod:" + new Date());
Thread.sleep(1000);
}
}
每隔 2 秒执行 1 次。
fixedRateMethod:Fri May 15 22:04:52 CST 2020
fixedRateMethod:Fri May 15 22:04:54 CST 2020
fixedRateMethod:Fri May 15 22:04:56 CST 2020
fixedRateMethod:Fri May 15 22:04:54 CST 2020
fixedRateMethod:Fri May 15 22:04:56 CST 2020
//任务类
@Component
public class MySpringTask {
//执行结束2秒后执行下次任务
@Scheduled(fixedDelay = 2000)
public void fixedDelayMethod() throws InterruptedException {
System.out.println("fixedDelayMethod:" + new Date());
Thread.sleep(1000);
}
}
@Component
public class MySpringTask {
//执行结束2秒后执行下次任务
@Scheduled(fixedDelay = 2000)
public void fixedDelayMethod() throws InterruptedException {
System.out.println("fixedDelayMethod:" + new Date());
Thread.sleep(1000);
}
}
每次打印后先等待 1 秒,然后方法执行结束 2 秒后再次执行任务
每 3 秒打印 1 行内容。
fixedDelayMethod:Fri May 15 22:08:26 CST 2020
fixedDelayMethod:Fri May 15 22:08:29 CST 2020
fixedDelayMethod:Fri May 15 22:08:32 CST 2020
fixedDelayMethod:Fri May 15 22:08:29 CST 2020
fixedDelayMethod:Fri May 15 22:08:32 CST 2020
@Scheduled 也支持使用 Cron 表达式
Cron 表达式
@Scheduled 也支持使用 Cron 表达式
Cron 表达式可以非常灵活地设置定时任务的执行时间。
Cron 表达式并不难理解,从左到右一共 6 个位置,分别代表秒、时、分、日、月、星期
以秒为例
如果该位置上是 0 ,表示在第 0 秒执行;
如果该位置上是 * ,表示每秒都会执行;
如果该位置上是 ? ,表示该位置的取值不影响定时任务,由于月份中的日和星期可能会发生意义冲突,所以日、 星期中需要有一个配置为 ? 。
如果该位置上是 * ,表示每秒都会执行;
如果该位置上是 ? ,表示该位置的取值不影响定时任务,由于月份中的日和星期可能会发生意义冲突,所以日、 星期中需要有一个配置为 ? 。
以本节开头的两个需求为例:
监控一个重点服务的运行状态,可以每隔 1 分钟调用下该服务的心跳接口,调用失败时即发出告警信息;
想在每天凌晨的时候,将所有商品的库存置满,以免早上忘记添加库存影响销售。
// 任务类
@Component
public class MySpringTask {
//在每分钟的00秒执行
@Scheduled(cron = "0 * * * * ?")
public void jump() throws InterruptedException {
System.out.println("心跳检测:" + new Date());
}
//在每天的00:00:00执行
@Scheduled(cron = "0 0 0 * * ?")
public void stock() throws InterruptedException {
System.out.println("置满库存:" + new Date());
}
}
@Component
public class MySpringTask {
//在每分钟的00秒执行
@Scheduled(cron = "0 * * * * ?")
public void jump() throws InterruptedException {
System.out.println("心跳检测:" + new Date());
}
//在每天的00:00:00执行
@Scheduled(cron = "0 0 0 * * ?")
public void stock() throws InterruptedException {
System.out.println("置满库存:" + new Date());
}
}
cron = "0 * * * * ?" 表示在每分钟的 00 秒执行、
cron = "0 0 0 * * ?" 表示在每天的 00:00:00 执行。
Quartz 定时任务
Spring Task 已经可以满足绝大多数项目对定时任务的需求,但是在企业级应用这个领域,还有更加强大灵活的 Quartz 框架可供选择。
Quartz 框架
举例
根据数据库中的配置,动态地指定商品打折的时间区间时,就可以利用 Quartz 框架来实现。
具体完整实现下
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-quartz
生成项目后导入 Eclipse 开发环境。
引入 Quartz 框架相关依赖
需要引入 Quartz 框架相关依赖。
<!-- Quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
开启定时任务,添加 @EnableScheduling 注解
同样需要,在启动类上添加 @EnableScheduling 注解,开启定时任务功能。
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class SpringBootQuartzApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootQuartzApplication.class, args);
}
}
@EnableScheduling // 开启定时任务
public class SpringBootQuartzApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootQuartzApplication.class, args);
}
}
Quartz 定时任务开发
Quartz 定时任务需要通过 Job 、 Trigger 、 JobDetail 来设置
Job:具体任务操作类
Trigger:触发器,设定执行任务的时间
JobDetail:指定触发器执行的具体任务类及方法
开发一个 Job 组件
//打折任务
@Component // 注册到容器中
public class DiscountJob {
//执行打折
public void execute() {
System.out.println("更新数据库中商品价格,统一打5折");
}
}
@Component // 注册到容器中
public class DiscountJob {
//执行打折
public void execute() {
System.out.println("更新数据库中商品价格,统一打5折");
}
}
然后在配置类中设定 Trigger 及 JobDetail 。
//定时任务配置
@Configuration
public class QuartzConfig {
//配置JobDetail工厂组件,生成的JobDetail指向discountJob的execute()方法
@Bean
MethodInvokingJobDetailFactoryBean jobFactoryBean() {
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("discountJob");
bean.setTargetMethod("execute");
return bean;
}
//触发器工厂
@Bean
CronTriggerFactoryBean cronTrigger() {
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
// Corn表达式设定执行时间规则
bean.setCronExpression("0 0 8 ? * 7");
// 执行JobDetail
bean.setJobDetail(jobFactoryBean().getObject());
return bean;
}
}
@Configuration
public class QuartzConfig {
//配置JobDetail工厂组件,生成的JobDetail指向discountJob的execute()方法
@Bean
MethodInvokingJobDetailFactoryBean jobFactoryBean() {
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("discountJob");
bean.setTargetMethod("execute");
return bean;
}
//触发器工厂
@Bean
CronTriggerFactoryBean cronTrigger() {
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
// Corn表达式设定执行时间规则
bean.setCronExpression("0 0 8 ? * 7");
// 执行JobDetail
bean.setJobDetail(jobFactoryBean().getObject());
return bean;
}
}
具体分析下上面的代码:
触发器设定的 Corn 表达式为 0 0 8 ? * 7 ,表示每周六的 08:00:00 执行 1 次;
触发器指定的 JobDetail 为 jobFactoryBean 工厂的一个对象,而 jobFactoryBean 指定的对象及方法为 discountJob 与 execute () ;
所以每周六的 8 点,就会运行 discountJob 组件的 execute () 方法 1 次;
Corn 表达式和执行任务、方法均以参数形式存在,这就意味着我们完全可以根据文件或数据库配置动态地调整执行时间和执行的任务;
最后,周六 8 点的时候,商品都打了 5 折,别忘了促销结束的时候恢复价格啊。
Spring Boot 使用拦截器
什么是拦截器
举例一个生活中非常形象的例子:反导系统
就像导弹要攻击目标的时候,可能会被先进的反导系统拦截,
此处的反导系统就是一种拦截器。
反导系统可以保护目标的安全并识别对目标的攻击行为。
在 API 方法的外围放置拦截器
开发的应用,对外暴露的是控制器中定义的 API 方法
可以在 API 方法的外围放置拦截器
所有对 API 的访问都可以通过拦截器进行过滤。
拦截有什么意义
跟踪对应用的访问行为,对合法访问行为予以放行,对非法访问行为予以拒绝。
Spring Boot 的拦截器
能够管理对控制器方法的访问请求
通过使用拦截器,可以实现访问控制,加强项目的安全性。
跟踪访问行为(实现 HandlerInterceptor 接口,重写多个钩子, 并配置)
要想实现对访问的拦截,首先要能跟踪访问行为,在 Spring Boot 中引入拦截器来实现下。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-interceptor
生成项目后导入 Eclipse 开发环境
引入项目依赖-spring-boot-starter-web
引入 Web 项目依赖即可。
<!-- web项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建拦截器(自定义拦截器类,实现 HandlerInterceptor 接口)
创建的类实现 HandlerInterceptor 接口,即可成为拦截器类.
//自定义拦截器类
public class MyInterceptor implements HandlerInterceptor {// 实现HandlerInterceptor接口
//访问控制器方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(new Date() + "--preHandle:" + request.getRequestURL());
return true;
}
//访问控制器方法后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println(new Date() + "--postHandle:" + request.getRequestURL());
}
//postHandle方法执行完成后执行,一般用于释放资源
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println(new Date() + "--afterCompletion:" + request.getRequestURL());
}
}
public class MyInterceptor implements HandlerInterceptor {// 实现HandlerInterceptor接口
//访问控制器方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(new Date() + "--preHandle:" + request.getRequestURL());
return true;
}
//访问控制器方法后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println(new Date() + "--postHandle:" + request.getRequestURL());
}
//postHandle方法执行完成后执行,一般用于释放资源
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println(new Date() + "--afterCompletion:" + request.getRequestURL());
}
}
在上面的实例中,定义了一个拦截器类 MyInterceptor
通过实现 HandlerInterceptor 接口,该类具备了拦截器的功能。
MyInterceptor 中的方法执行顺序为 preHandle – Controller 方法 – postHandle – afterCompletion
拦截器实际上可以对 Controller 方法执行前后进行拦截监控。
注意preHandle的返回值
preHandle 需要返回布尔类型的值。
如果preHandle 返回 true 时,对控制器方法的请求才能到达控制器,继而到达 postHandle 和 afterCompletion 方法;
如果 preHandle 返回 false ,后面的方法都不会执行。
配置拦截器继承WebMvcConfigurer,让配置器生效
上一步开发了配置器类
如果想让配置器生效,还需要通过配置类进行相应配置。
//Web配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
//添加Web项目的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有访问路径,都通过MyInterceptor类型的拦截器进行拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
//添加Web项目的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有访问路径,都通过MyInterceptor类型的拦截器进行拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
创建控制器检验拦截器的效果
建立一个简单的控制器,实现登录方法,以便检验拦截器的效果。
//登录控制器
@RestController
public class LoginController {
@RequestMapping("/login")
public boolean login(String username, String password) {
System.out.println(new Date() + " 某用户尝试登录,用户名:" + username + " 密码:" + password);
return true;
}
}
@RestController
public class LoginController {
@RequestMapping("/login")
public boolean login(String username, String password) {
System.out.println(new Date() + " 某用户尝试登录,用户名:" + username + " 密码:" + password);
return true;
}
}
跟踪访问行为
运行启动类,访问 http://127.0.0.1:8080/login?username=imooc&password=123,控制台输出如下:
控制台输出内容
可见我们已经完整的跟踪了一次对 http://127.0.0.1:8080/login 接口的访问。
实现访问控制(实现 HandlerInterceptor 接口,重写preHandle钩子, 并配置)
区分合法、非法访问,最常见的就是根据用户的登录状态、角色判断。
对未登录用户非法访问请求的拦截。
修改控制器方法
修改登录方法,当用户输入的用户名和密码正确时,通过 Session 记录登录人信息。
然后开发获取登录人员信息方法,返回 Session 中记录的登录人信息。
//登录控制器
@RestController
public class LoginController {
//登录方法
@RequestMapping("/login")
public boolean login(HttpServletRequest request, String username, String password) {
if ("imooc".equals(username) && "123".equals(password)) {
// 登录成功,则添加Session并存储登录用户名
request.getSession().setAttribute("LOGIN_NAME", username);
return true;
}
return false;
}
// 获取登录人员信息
@RequestMapping("/info")
public String info(HttpServletRequest request) {
return "您就是传说中的:" + request.getSession().getAttribute("LOGIN_NAME");
}
}
@RestController
public class LoginController {
//登录方法
@RequestMapping("/login")
public boolean login(HttpServletRequest request, String username, String password) {
if ("imooc".equals(username) && "123".equals(password)) {
// 登录成功,则添加Session并存储登录用户名
request.getSession().setAttribute("LOGIN_NAME", username);
return true;
}
return false;
}
// 获取登录人员信息
@RequestMapping("/info")
public String info(HttpServletRequest request) {
return "您就是传说中的:" + request.getSession().getAttribute("LOGIN_NAME");
}
}
修改拦截器方法
由于用户在登录之前还没有设置 Session ,所以登录方法不应该拦截,可以让用户自由请求。
但是只有登录成功后的用户,也就是说,具备 Session 的用户,才能访问 info 方法。
//自定义拦截器类
public class MyInterceptor implements HandlerInterceptor {// 实现HandlerInterceptor接口
//访问控制器方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (request.getRequestURI().contains("/login") == true) {// 登录方法直接放行
return true;
} else {// 其他方法需要先检验是否存在Session
if (request.getSession().getAttribute("LOGIN_NAME") == null) {//未登录的不允许访问
return false;
} else {
return true;
}
}
}
}
public class MyInterceptor implements HandlerInterceptor {// 实现HandlerInterceptor接口
//访问控制器方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (request.getRequestURI().contains("/login") == true) {// 登录方法直接放行
return true;
} else {// 其他方法需要先检验是否存在Session
if (request.getSession().getAttribute("LOGIN_NAME") == null) {//未登录的不允许访问
return false;
} else {
return true;
}
}
}
}
测试验证
直接info接口,访问被拦截
直接请求 http://127.0.0.1:8080/info接口
由于此时未登录,所以请求被拦截,网页输出如下:
访问被拦截
先请求登录方法,再访问info接口 ,则网页正常输出
登录成功后,访问正常通过拦截器
请求登录方法 http://127.0.0.1:8080/login?username=imooc&password=123
访问 http://127.0.0.1:8080/info
Spring Boot 监控与报警
提出背景
因为公司开发的项目多、为客户部署的项目实例多。
工作中都会经常遇到,由于某个客户的项目突然无法访问,一堆研发、售后部门的同事火急火燎处理问题的场景。
在 Spring Boot 中实现这种监控和报警的功能非常简单,可以直接利用 Spring Boot Admin 实现可视化监控
功能清单
【可视化界面】
可视化监控可以提供一种总揽全局的监控视角,
众多项目运行状况一目了然,做到心里有底。
能够有一个界面,监控所有关注的项目实例运行状态
【监控详细参数】
重点项目可以定期监控项目实例的详细运行参数。
监控某个项目实例的各项运行参数
内存占用情况
磁盘使用情况
数据库连接情况
为上线运行的项目提供额外的保障机制。
通过分析参数信息,预测性能瓶颈发生的时间节点,提前采取扩容等措施,防患于未然。
【自动报警】
报警和监控机制可以为上线运行的项目提供额外的保障机制。
项目实例因各种原因关闭时,可以自动报警。
当发生严重错误,或者由于人为误操作导致项目离线时,指定邮箱能够及时收到报警信息。
技术人员及时排查处理,作为项目运维方心里更有底。
利用 Spring Boot Admin 实现可视化监控
可以直接利用 Spring Boot Admin 实现可视化监控
此时至少需要两个项目实例
一个是监控的管理端
一个是被监控的客户端
构建监控管理端项目
初始化项目
打开 Spring Initializr , Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-monitor-manager
生成项目后导入 Eclipse 开发环境。
引入管理端项目依赖
监控管理端需要使用网页展示监控信息,所以引入 Web 依赖,
另外添加 Spring Boot Admin 管理端依赖项。
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Admin 管理端依赖项 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Admin 管理端依赖项 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.3</version>
</dependency>
添加 @EnableAdminServer 注解,开启监控管理端
在启动类上添加 @EnableAdminServer 注解开启 Spring Boot Admin 监控管理功能,代码如下:
@SpringBootApplication
@EnableAdminServer // 开启监控管理
public class SpringBootMonitorManagerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMonitorManagerApplication.class, args);
}
}
@EnableAdminServer // 开启监控管理
public class SpringBootMonitorManagerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMonitorManagerApplication.class, args);
}
}
然后运行启动类,访问 http://127.0.0.1:8080 会发现界面上已经显示监控信息了。
Spring Boot Admin 监控管理页面
构建监控客户端项目
初始化项目
打开 Spring Initializr , Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-monitor-client
生成项目后导入 Eclipse 开发环境。
引入客户端项目依赖
直接引入 Web 依赖和监控客户端依赖。
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Admin监控客户端依赖 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Admin监控客户端依赖 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.3</version>
</dependency>
修改客户端的配置文件 application.properties
修改客户端的配置文件 application.properties ,以便指定客户端指向的服务端的地址。
总览配置文件
# 配置端口
server.port=8091
# 配置监控管理端地址
spring.boot.admin.client.url=http://127.0.0.1:8080
# 客户端的名称,用于区分不同的客户端
spring.boot.admin.client.instance.name=CLIENT1
server.port=8091
# 配置监控管理端地址
spring.boot.admin.client.url=http://127.0.0.1:8080
# 客户端的名称,用于区分不同的客户端
spring.boot.admin.client.instance.name=CLIENT1
(1)修改端口号
由于刚刚服务端已经占用了 8080 端口
将客户端的端口设置为 8091
(2)指定监控管理端地址
此处指定监控管理端地址使用的是 spring.boot.admin.client.url ,
我个人认为应使用 spring.boot.admin.server.url 更加合理。
(3)设置是客户端的名称
还有一个必要设置是客户端的名称
当监控的项目实例比较多时,需要通过客户端名称来区分。
测试监控效果
启动客户端程序,然后刷新服务端网页,会发现监控管理页面已经显示了客户端信息。
监控页面展示客户端信息
此时,关闭客户端程序,然后稍等一会刷新下监控管理页面,会发现监控管理页已经显示了离线项目实例信息。
(注意服务端发现客户端离线是需要一定时间的)
监控页面展示离线客户端信息
监控实例运行参数(通过配置文件指定展示哪些信息)
为什么默认显示的项目实例信息较少
使用 Spring Boot Admin 后,默认显示的项目实例信息比较少。
CLIENT1 实例默认显示信息
默认设置主要是为了保证项目实例的安全性,只展示了非常少的信息,
如何展示更多的监控实例参数信息?
可以通过配置文件指定展示哪些信息,如下。
# 配置客户端展示哪些信息,*表示展示全部信息
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.include=*
此时刷新监控管理页,会发现已经展示各类运行参数信息。
全面展示项目实例运行参数信息
不同项目需要展示的信息不同,可以通过 management.endpoints.web.exposure.include 配置项进行设置
自动报警
可视化监控提供了全面了解项目运行状况的手段,但是我们不可能 7*24 小时盯着界面看哪个应用离线了。
最妙的效果是,项目离线时自动通知售后、运维等相关技术人员。
Spring Boot Admin 也提供了自动报警的功能,简直太完美了,接下来我们来实现下。
最妙的效果是,项目离线时自动通知售后、运维等相关技术人员。
Spring Boot Admin 也提供了自动报警的功能,简直太完美了,接下来我们来实现下。
1 引入依赖项
为监控服务端项目引入邮件依赖。
<!-- 邮件依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置邮件发送所需信息
只需要配置常规的邮件收发信息即可。
# 网易邮箱发件服务器
spring.mail.host=smtp.163.com
# 网易邮箱发件端口
spring.mail.prot=25
# 发件人账号
spring.mail.username=taqsxxkj@163.com
# 发件授权密码,注意授权码是用于登录第三方邮件客户端的专用密码
spring.mail.password=123456
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# Spring Boot Admin 发件收件信息
spring.boot.admin.notify.mail.from=taqsxxkj@163.com
spring.boot.admin.notify.mail.to=taqsxxkj@163.com
spring.boot.admin.notify.mail.cc=taqsxxkj@163.com
spring.mail.host=smtp.163.com
# 网易邮箱发件端口
spring.mail.prot=25
# 发件人账号
spring.mail.username=taqsxxkj@163.com
# 发件授权密码,注意授权码是用于登录第三方邮件客户端的专用密码
spring.mail.password=123456
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# Spring Boot Admin 发件收件信息
spring.boot.admin.notify.mail.from=taqsxxkj@163.com
spring.boot.admin.notify.mail.to=taqsxxkj@163.com
spring.boot.admin.notify.mail.cc=taqsxxkj@163.com
此处特别注意发件授权密码不是普通邮箱的登录密码,而是授权密码,以网易邮箱为例在下图位置设置。
网易邮箱授权密码设置
项目实例离线邮件报警
启动监控服务端和客户端,然后关闭客户端,稍等一会检查指定的报警接收邮箱,就会发现已收到报警邮件了。
项目实例离线邮件报警内容
Spring Boot 安全管理
安全管理说明
安全管理是软件系统必不可少的的功能。
根据经典的“墨菲定律”——凡是可能,总会发生。
如果系统存在安全隐患,最终必然会出现问题。
对于更加复杂的安全管理续期, Spring Boot 也可以快速的整合 Spring Security 或 Shiro ,以构建企业级的安全管理体系
Spring Boot 整合 Spring Security ,实际上大部分工作都在安全管理配置类上。
通过安全管理配置类,将用户、密码及其对应的权限信息放入容器,同时将访问路径所需要的权限信息放入容器
Spring Security 就会按照逻辑实施权限管理了
用户访问路径
判断所需权限
用户是否具备该权限
允许或拒绝访问
如何使用 Spring Boot + Spring Security 开发前后端分离的权限管理功能。
Spring Security 用法简介
作为一个知名的安全管理框架, Spring Security 对安全管理功能的封装已经非常完整了。
在使用时,只需
(1)从配置文件或者数据库中,把用户、权限相关的信息取出来
(2)通过配置类方法告知
系统初始化时,告知访问路径所需要的对应权限。
登录时,告知真实用户名和密码。
登录成功时,告知当前用户具备的权限。
用户访问接口时,Spring Security自动判断能否访问。
因为已经知道用户具备的权限
也知道访问路径需要的对应权限
Spring Security 就能自动实现认证、授权等安全管理操作了。
数据库模块实现
定义表结构
表结构
数据库表结构
api 表中的 path 字段表示接口的访问路径,另外所有的 id 都是自增主键。
4 张表
用户表 user:保存用户名、密码,及用户拥有的角色 id 。
角色表 role :保存角色 id 与角色名称。
角色权限表 roleapi:保存角色拥有的权限信息。
权限表 api:保存权限信息,在前后端分离的项目中,权限指的是控制器中的开放接口。
角色表 role :保存角色 id 与角色名称。
角色权限表 roleapi:保存角色拥有的权限信息。
权限表 api:保存权限信息,在前后端分离的项目中,权限指的是控制器中的开放接口。
构造测试数据
执行如下 SQL 语句插入测试数据,
-- 用户
INSERT INTO `user` VALUES (1, 'admin', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 1);
INSERT INTO `user` VALUES (2, 'guest', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 2);
-- 角色
INSERT INTO `role` VALUES (1, '管理员');
INSERT INTO `role` VALUES (2, '游客');
-- 角色权限
INSERT INTO `roleapi` VALUES (1, 1, 1);
INSERT INTO `roleapi` VALUES (2, 1, 2);
INSERT INTO `roleapi` VALUES (3, 2, 1);
-- 权限
INSERT INTO `api` VALUES (1, 'viewGoods');
INSERT INTO `api` VALUES (2, 'addGoods');
INSERT INTO `user` VALUES (1, 'admin', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 1);
INSERT INTO `user` VALUES (2, 'guest', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 2);
-- 角色
INSERT INTO `role` VALUES (1, '管理员');
INSERT INTO `role` VALUES (2, '游客');
-- 角色权限
INSERT INTO `roleapi` VALUES (1, 1, 1);
INSERT INTO `roleapi` VALUES (2, 1, 2);
INSERT INTO `roleapi` VALUES (3, 2, 1);
-- 权限
INSERT INTO `api` VALUES (1, 'viewGoods');
INSERT INTO `api` VALUES (2, 'addGoods');
用户密码是 123 加密后的值
语句指定了 admin 用户可以访问 viewGoods 和 addGoods 接口,而 guest 用户只能访问 viewGoods 接口。
Spring Boot 后端实现
新建一个 Spring Boot 项目,并利用 Spring Security 实现安全管理功能。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-security
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖、安全管理依赖,由于要访问数据库所以引入 JDBC 和 MySQL 依赖。
<!-- Web项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 安全管理依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 安全管理依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
定义数据对象(UserDo)
安全管理,肯定需要从数据库中读取用户信息,以便判断用户登录名、密码是否正确,所以需要定义用户数据对象。
public class UserDo {
private Long id;
private String username;
private String password;
private String roleId;
// 省略 get set
}
private Long id;
private String username;
private String password;
private String roleId;
// 省略 get set
}
开发数据访问类,从数据库获取权限列表
系统初始化时,告诉 Spring Security 访问路径所需要的对应权限,所以开发从数据库获取权限列表的方法。
@Repository
public class ApiDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//获取所有api
public List<String> getApiPaths() {
String sql = "select path from api";
return jdbcTemplate.queryForList(sql, String.class);
}
}
public class ApiDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//获取所有api
public List<String> getApiPaths() {
String sql = "select path from api";
return jdbcTemplate.queryForList(sql, String.class);
}
}
登录时,告诉 Spring Security 真实用户名和密码。 登录成功时,告诉 Spring Security 当前用户具备的权限。
所以我们开发根据用户名获取用户信息和根据用户名获取其可访问的 api 列表方法。
所以我们开发根据用户名获取用户信息和根据用户名获取其可访问的 api 列表方法。
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据用户名获取用户信息
public List<UserDo> getUsersByUsername(String username) {
String sql = "select id, username, password from user where username = ?";
return jdbcTemplate.query(sql, new String[] { username }, new BeanPropertyRowMapper<>(UserDo.class));
}
//根据用户名获取其可访问的api列表
public List<String> getApisByUsername(String username) {
String sql = "select path from user left join roleapi on user.roleId=roleapi.roleId left join api on roleapi.apiId=api.id where username = ?";
return jdbcTemplate.queryForList(sql, new String[] { username }, String.class);
}
}
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据用户名获取用户信息
public List<UserDo> getUsersByUsername(String username) {
String sql = "select id, username, password from user where username = ?";
return jdbcTemplate.query(sql, new String[] { username }, new BeanPropertyRowMapper<>(UserDo.class));
}
//根据用户名获取其可访问的api列表
public List<String> getApisByUsername(String username) {
String sql = "select path from user left join roleapi on user.roleId=roleapi.roleId left join api on roleapi.apiId=api.id where username = ?";
return jdbcTemplate.queryForList(sql, new String[] { username }, String.class);
}
}
开发服务类
开发 SecurityService 类,保存安全管理相关的业务方法。
@Service
public class SecurityService {
@Autowired
private UserDao userDao;
@Autowired
private ApiDao apiDao;
public List<UserDo> getUserByUsername(String username) {
return userDao.getUsersByUsername(username);
}
public List<String> getApisByUsername(String username) {
return userDao.getApisByUsername(username);
}
public List<String> getApiPaths() {
return apiDao.getApiPaths();
}
}
public class SecurityService {
@Autowired
private UserDao userDao;
@Autowired
private ApiDao apiDao;
public List<UserDo> getUserByUsername(String username) {
return userDao.getUsersByUsername(username);
}
public List<String> getApisByUsername(String username) {
return userDao.getApisByUsername(username);
}
public List<String> getApiPaths() {
return apiDao.getApiPaths();
}
}
开发控制器类
开发控制器类,其中 notLogin 方法是用户未登录时调用的方法,其他方法与权限表中的 api 一一对应。
@RestController
public class TestController {
//未登录时调用该方法
@RequestMapping("/notLogin")
public ResultBo notLogin() {
return new ResultBo(new Exception("未登录"));
}
//查看商品
@RequestMapping("/viewGoods")
public ResultBo viewGoods() {
return new ResultBo<>("viewGoods is ok");
}
//添加商品
@RequestMapping("/addGoods")
public ResultBo addGoods() {
return new ResultBo<>("addGoods is ok");
}
}
public class TestController {
//未登录时调用该方法
@RequestMapping("/notLogin")
public ResultBo notLogin() {
return new ResultBo(new Exception("未登录"));
}
//查看商品
@RequestMapping("/viewGoods")
public ResultBo viewGoods() {
return new ResultBo<>("viewGoods is ok");
}
//添加商品
@RequestMapping("/addGoods")
public ResultBo addGoods() {
return new ResultBo<>("addGoods is ok");
}
}
由于是前后端分离的项目,为了便于前端统一处理,我们封装了返回数据业务逻辑对象 ResultBo 。
public class ResultBo<T> {
//错误码 0表示没有错误(异常) 其他数字代表具体错误码
private int code;
//后端返回消息
private String msg;
//后端返回的数据
private T data;
//无参数构造函数
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
//带数据data构造函数
public ResultBo(T data) {
this();
this.data = data;
}
//存在异常的构造函数
public ResultBo(Exception ex) {
this.code = 99999;// 其他未定义异常
this.msg = ex.getMessage();
}
}
//错误码 0表示没有错误(异常) 其他数字代表具体错误码
private int code;
//后端返回消息
private String msg;
//后端返回的数据
private T data;
//无参数构造函数
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
//带数据data构造函数
public ResultBo(T data) {
this();
this.data = data;
}
//存在异常的构造函数
public ResultBo(Exception ex) {
this.code = 99999;// 其他未定义异常
this.msg = ex.getMessage();
}
}
开发 Spring Security 配置类
现在,需要将用户、权限等信息通过配置类告知 Spring Security 了。
定义配置类,注解 @EnableWebSecurity 开启安全管理功能
定义 Spring Security 配置类,通过注解 @EnableWebSecurity 开启安全管理功能。
@Configuration
@EnableWebSecurity // 开启安全管理
public class SecurityConfig {
@Autowired
private SecurityService securityService;
}
@EnableWebSecurity // 开启安全管理
public class SecurityConfig {
@Autowired
private SecurityService securityService;
}
注册密码加密组件
Spring Security 提供了很多种密码加密组件,使用官方推荐的 BCryptPasswordEncoder ,直接注册为 Bean 即可。
之前在数据库中预定义的密码字符串即为 123 加密后的结果。
Spring Security 在验证密码时,会自动调用注册的加密组件,将用户输入的密码加密后再与数据库密码比对。
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
//输出 $2a$10$kLQpA8S1z0KdgR3Cr6jJJ.R.QsIT7nrCdAfsF4Of84ZBX2lvgtbE.
System.out.println(new BCryptPasswordEncoder().encode("123"));
}
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
//输出 $2a$10$kLQpA8S1z0KdgR3Cr6jJJ.R.QsIT7nrCdAfsF4Of84ZBX2lvgtbE.
System.out.println(new BCryptPasswordEncoder().encode("123"));
}
将用户密码及权限告知 Spring Security
通过注册 UserDetailsService 类型的组件,组件中设置用户密码及权限信息即可。
@Bean
public UserDetailsService userDetailsService() {
return username -> {
List<UserDo> users = securityService.getUserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("用户名错误");
}
String password = users.get(0).getPassword();
List<String> apis = securityService.getApisByUsername(username);
// 将用户名username、密码password、对应权限列表apis放入组件
return User.withUsername(username).password(password).authorities(apis.toArray(new String[apis.size()]))
.build();
};
}
public UserDetailsService userDetailsService() {
return username -> {
List<UserDo> users = securityService.getUserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("用户名错误");
}
String password = users.get(0).getPassword();
List<String> apis = securityService.getApisByUsername(username);
// 将用户名username、密码password、对应权限列表apis放入组件
return User.withUsername(username).password(password).authorities(apis.toArray(new String[apis.size()]))
.build();
};
}
设置访问路径需要的权限信息
同样,通过注册组件,将访问路径需要的权限信息告知 Spring Security 。
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 开启跨域支持
httpSecurity.cors();
// 从数据库中获取权限列表
List<String> paths = securityService.getApiPaths();
for (String path : paths) {
/* 对/xxx/**路径的访问,需要具备xxx权限
例如访问 /addGoods,需要具备addGoods权限 */
httpSecurity.authorizeRequests().antMatchers("/" + path + "/**").hasAuthority(path);
}
// 未登录时自动跳转/notLogin
httpSecurity.authorizeRequests().and().formLogin().loginPage("/notLogin")
// 登录处理路径、用户名、密码
.loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password")
.permitAll()
// 登录成功处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, Authentication authentication)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ResultBo result = new ResultBo<>();
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = httpServletResponse.getWriter();
out.write(mapper.writeValueAsString(result));
out.close();
}
})
// 登录失败处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ResultBo result = new ResultBo<>(new Exception("登录失败"));
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = httpServletResponse.getWriter();
out.write(mapper.writeValueAsString(result));
out.flush();
out.close();
}
});
// 禁用csrf(跨站请求伪造)
httpSecurity.authorizeRequests().and().csrf().disable();
}
};
}
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 开启跨域支持
httpSecurity.cors();
// 从数据库中获取权限列表
List<String> paths = securityService.getApiPaths();
for (String path : paths) {
/* 对/xxx/**路径的访问,需要具备xxx权限
例如访问 /addGoods,需要具备addGoods权限 */
httpSecurity.authorizeRequests().antMatchers("/" + path + "/**").hasAuthority(path);
}
// 未登录时自动跳转/notLogin
httpSecurity.authorizeRequests().and().formLogin().loginPage("/notLogin")
// 登录处理路径、用户名、密码
.loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password")
.permitAll()
// 登录成功处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, Authentication authentication)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ResultBo result = new ResultBo<>();
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = httpServletResponse.getWriter();
out.write(mapper.writeValueAsString(result));
out.close();
}
})
// 登录失败处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ResultBo result = new ResultBo<>(new Exception("登录失败"));
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = httpServletResponse.getWriter();
out.write(mapper.writeValueAsString(result));
out.flush();
out.close();
}
});
// 禁用csrf(跨站请求伪造)
httpSecurity.authorizeRequests().and().csrf().disable();
}
};
}
按上面的设计,当用户发起访问时
通过 /login 访问路径可以发起登录请求,用户名和密码参数名分别为 username 和 password 。
未登录的访问会自动跳转到/notLogin 访问路径。
通过 /login 访问路径可以发起登录请求,用户名和密码参数名分别为 username 和 password 。
登录成功或失败会返回 ResultBo 序列化后的 JSON 字符串,包含登录成功或失败信息。
访问 /xxx 形式的路径,需要具备 xxx 权限。
用户所具备的权限已经通过上面的 UserDetailsService 组件告知 Spring Security 了。
使用 PostMan 进行功能的验证测试
启动项目后,使用 PostMan 进行验证测试。
未登录测试
在未登录时,直接访问控制器方法,会自动跳转 /notLogin 访问路径,返回未登录提示信息。
未登录测试
错误登录密码测试
调用登录接口,当密码不对时,返回登录失败提示信息。
错误登录密码测试
以 guest 用户登录
使用 guest 用户及正确命名登录,返回操作成功提示信息。
以 guest 用户登录
guest 用户访问授权接口
按照数据库中定义的规则, guest 用户可以访问 viewGoods 接口方法
guest 用户访问授权接口
guest 用户访问未授权接口
按照数据库中定义的规则, guest 没有访问 addGoods 接口方法的权限。
guest 用户访问未授权接口
admin 用户登录及访问授权接口
按照数据库中定义的规则, admin 用户登录后可以访问 viewGoods 和 addGoods 两个接口方法。
admin 用户登录
admin 用户访问授权接口
admin 用户访问授权接口
Spring Boot 集成第三方框架与应用场景
Spring Boot AOP 应用场景
Spring Boot 缓存应用场景
(基于 ConcurrenMapCacheManager 缓存管理器)Spring Boot 默认缓存
使用 Ehcache 缓存
使用 Redis 缓存
Spring Boot RabbitMQ 应用场景
Spring Boot应用实战(IOV CMS平台)
Spring Boot应用实战(IOV CMS平台)
Spring Boot常用组件简介
通技选型:
布局排版:Boostrap
表格:DataTable(Boostrap Table in future)
树形菜单:JSTree
整体UI排版布局:H+
图表:百度Echarts
文件上传:百度WebUploader
表格:DataTable(Boostrap Table in future)
树形菜单:JSTree
整体UI排版布局:H+
图表:百度Echarts
文件上传:百度WebUploader
Spring Boot原理
SpringBoot常见问题
SpringBoot中如何引入定时任务?
SpringBoot如何抛异常?
SpringBoot是如何实现自动化配置的?
SpringBoot的了解?
Spring与SpringBoot的区别
为什么你会用SpringBoot。SpringBoot的好处是?
0 条评论
下一页