【Java】Spring Boot 教程

Spring Boot 基础开发

Spring Boot 简介

  1. 前言

每逢春暖花开的时节,我都会想起大学时代。那时候的我,在阳光明媚的日子里,坐在图书馆的落地窗前。桌子上是一叠 Java Web 书本,还有我那破破却可爱的笔记本电脑。
在这里插入图片描述
那是 SSH 风华正茂的年代,Spring 如日中天,负责整合各种框架,俨然一副老大哥的样子;Hibernate 是数据持久层的不二之选,iBatis 在它面前就像个小老弟;Struts 则是 MVC 框架的形象代言,不懂点 Struts 都不好意思说在做 Web 开发。

而我却总是,被 SSH 繁琐的配置困扰。SSH 各有一大堆配置,当他们碰到一起,还需要额外互相配置。就像三个老朋友,每次再重逢,还要互相介绍。

做一个简单的项目,竟有一大半时间在配置。不是在编辑配置文件的路上,就是在修复配置错误的途中。

程序开发不应该是简单而优雅的吗?正如我们所追求的生活。

  1. Spring 的诞生

实际上,让开发变得简单,是 Spring 诞生的原动力。

Java 官方推出的企业级开发标准是 EJB ,但 EJB 是相当臃肿、低效的,且难以测试,把当时的 Java 开发者折腾得不轻。
在这里插入图片描述

Spring 官网介绍:让 Java 变简单
那时候,国外有一个年轻的小伙 Rod Johnson,对 SSH 的繁琐产生了质疑。他不光质疑,还去做了他认为对的事情。

经过不断的经验总结和实践,他在 2004 年推出了经典力作《Expert one-on-one J2EE Development without EJB》。该书奠定了 Spring 框架的思想基础,把 EJB 的种种缺点逐一否定,还提出了简洁的替代方案。

从此 Rod Johnson 和 Spring 框架一炮而红,其影响之深远,恐怕连 Rod Johnson 自己都想不到吧。

有时候,不要过于迷信官方,也要敢于思考和质疑。实践是检验真理的唯一标准,编程也不外乎是。

  1. Spring 的发展

随着 Spring 的流行,Spring 团队也深感责任重大。Spring 团队对 Spring 的优化工作也从未停歇,从 Spring1.x 到现在的 Spring5.x,每一个版本号都是进化的脚印。

最开始的时候,Spring 只支持基于 XML 的配置,后来又陆续增加了对注解配置、Java 类配置的支持。

但是无论怎么变换,都需要开发人员手工去配置,而这些配置往往千篇一律,令人乏味。

我们驾驶汽车,默认都是车窗关闭、空调关闭、仪表盘开启这样的设置。如果每次进入汽车,都要手工逐一设置一遍,其实完全没有必要。

同理,既然大多数人开发 Spring 应用,都有默认的习惯。那何不直接提供默认配置,项目启动时自动采用默认配置,只有当需要个性化功能时,再去手工配置。

所以,在 2014 年,一个叫 Spring Boot 的框架,就这么出现了。

  1. Spring Boot 的由来
    Spring Boot 为简化 Spring 应用开发而生,Spring Boot 中的 Boot 一词,即为快速启动的意思。Spring Boot 可以在零配置情况下一键启动,简洁而优雅。

为了让 Spring 开发者痛快到底,Spring 团队做了以下设计:

  • 简化依赖,提供整合的依赖项,告别逐一添加依赖项的烦恼;
  • 简化配置,提供约定俗成的默认配置,告别编写各种配置的繁琐;
  • 简化部署,内置 servlet 容器,开发时一键即运行。可打包为 jar 文件,部署时一行命令即启动;
  • 简化监控,提供简单方便的运行监控方式。

基于以上设计目的,Spring 团队推出了 Spring Boot 。

  1. Spring Boot 的江湖地位

由于 Spring Boot 设计优雅,实现简单,可以节省不少开发时间。

另外由于微服务的火爆,作为 Spring Cloud 实现基础的 Spring Boot ,更是春风得意,风头一时无两。
在这里插入图片描述

从 Spring Boot 在 Spring 官网的菜单位置,可以一瞥 Spring Boot 的地位
所以不管出于哪种目的,为跳槽、为加薪、为方便、为省心、为学习、为进步、为爱情、为家庭,Spring Boot 都是 Java 开发旅途的重要风景。

而我,本系列文章的作者,愿陪你看万山红遍、层林尽染,用尽量轻松的语言,讲一些编程的故事和经验,陪你度过一段愉快的 Spring Boot 学习时光。

  1. Spring Boot 的学习基础

Spring Boot 非常好用,但是并不是 0 基础就可以直接上手的。

Java 语言基础是必备的,这个不必赘述。

在学习 Spring Boot 之前,最好是已经对 Spring 及 Spring MVC 框架有一定的了解。Spring Boot 是一个快速开发框架,其技术基础几乎全部来源自 Spring 。

所以本系列教程的学习基础,是 Java 、 Spring 及 Spring MVC 。其中 Spring MVC 是 Spring 大家庭的非常重要的一员,所以此处单独拿出来强调下。

  1. 小结

Spring Boot 简单易用,可以快速上手,迅速提高开发效率,值得学习!

Spring Boot 第一个项目

  1. 前言

Spring Boot 可以使用 Maven 构建,遵循 Maven 的项目结构规范,项目结构是模板化的,基本都一模一样。

模板化的东西可以自动生成,Spring 官方就提供了 Spring Initializr 。它能自动生成 Spring Boot 项目,我们直接导入到开发工具使用即可。

  1. 生成 Spring Boot 项目

打开 Spring Initializr 网址 http://start.spring.io ,根据我们项目的情况填入以下信息。

在这里插入图片描述

Spring Initializr 生成 Spring Boot 项目
这是第一次接触 Spring Initializr ,我们来详细了解界面上选项的作用。

  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 项目了。

  11. 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 就是通过配置文件得知项目构建规则的。

Tips: 此处有同学要发问了,不是说好 Spring Boot 没有配置文件吗?不要着急,Spring Boot
可以在没有配置文件时照常运行。但如果需要个性化功能的话,就会用到配置文件了。 Spring Boot 的配置文件使用非常简单,放心就是了!

  1. pom.xml 详解
    大家可能也发现了,到此刻为止,Spring Boot 项目也没啥新鲜的。都是之前了解过的东西,跟普通的 Maven 项目也没啥区别。

其实真正的变化在 pom.xml 中,我们马上打开瞧一瞧。因为 pom.xml 配置比较长,我们从头到尾分段解释下。

4.1 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>

4.2 Spring Boot 版本配置
这一段配置代码,指定使用 Spring Boot 2.2.5.RELEASE 版本 。如果我们要更换 Spring Boot 版本,只需要修改 标签中间的版本号部分即可。

实例:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

4.3 项目信息配置
这一段配置代码,大家看到应该比较眼熟,内容即为之前使用 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>

Tips: name 是项目的名称,不用特别严谨。而 artifactId 是用来区分 group
下面的子项目的,需要保证严格唯一。一般情况下将 artifactId 和 name 设置成一样的就可以了。

4.4 依赖配置
接下来,这一段代码配置,负责指定 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>

Tips: 可以看到上面两个依赖我们并没有指定版本号,其实是因为 Spring Boot 2.2.5 已经有默认的依赖项版本号了。这是通过
Maven 父继承实现的,即 标签配置部分,这个稍作了解即可。

4.5 插件配置
最后的这一段代码配置,指定了一个插件,用来构建、运行、打包 Spring Boot 项目。

实例:

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

  1. 小结
    本章先讲了 Spring Boot 项目的构建方法,然后大体描述了 Spring Boot 项目的结构和配置文件,让大家有一个总体的感性认识。

实际使用中, Spring Boot 是高度封装的,我们开箱即用即可。在学习阶段,用不着了解很多的原理。

就像你开汽车,会挂挡就行,不需要知道变速箱是啥工作原理。

框架封装的目的就是为了傻瓜式使用, Spring Boot 就是这样的一个傻瓜式工具框架。等哪一天用得很溜了,再去研究原理也不迟。

Spring Boot 项目启动机制

  1. 前言

很多同学,学了很久的 Spring ,也用了很久的 Spring ,却还是不知道 Spring 是什么?Spring 中 XML / 注解 / Java 类三种配置方式,有什么区别和联系。

上面两个问题,正是理解 Spring Boot 的关键!

Spring 本质上是一个容器,里面存放的是 Java 对象,放入容器的 Java 对象被称为 Spring 组件(Bean)。

而 XML / 注解 / Java 类三种配置方式,只是形式不同,目的都是在容器中注册 Bean 。三种方式可以同时使用,只是需要注意, Bean 命名不要发生冲突。

当我们使用 Spring Boot 时会有变化吗?实际上,容器还是那个容器,配置也还是那三种配置。当然 Spring Boot 本身就是为了简化配置,所以基本不再使用 XML 配置方式了。
Spring Boot 项目生成后,只有简简单单一个类,简单优雅,赏心悦目!

实例:

@SpringBootApplication
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 启动的秘密就在这个注解上了。

  1. 神奇的 @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 {
}

看起来很复杂,其实就是一个组合注解,包含了多个注解的功能,咱们来分析一下。

首先是 @SpringBootConfiguration 注解,它继承自 @Configuration 注解,功能也跟 @Configuration 一样。它会将当前类标注为配置类了,我们在启动类中配置 Bean 就可以生效了。

其次是 @ComponentScan 注解,用来指定我们要扫描的包,以便发现 Bean 。注意在默认情况下, SpringBoot 扫描该注解标注类所在包及其子包。当我们的控制器、服务类等 Bean 放到不同的包中时,就需要通过 @ComponentScan 注解指定这些包,以便发现 Bean 。

最重要的是 @EnableAutoConfiguration 注解,用来启动自动配置。开启自动配置后, Spring Boot 会扫描项目中所有的配置类,然后根据配置信息启动 Spring 容器。

拥有了 @SpringBootConfiguration ,我们就拥有了一个可以拿来即用的 Spring 容器环境了。

Spring Boot 数据访问

Spring Boot 集成 MyBatis

  1. 前言

企业级应用数据持久层框架,最常见的应该是 Hibernate 和 MyBatis 。

Hibernate 是相当彻底的 ORM 对象 - 关系映射框架,使用 Hibernate ,开发者可以不考虑 SQL 语句的编写与执行,直接操作对象即可。

与 Hibernate 相比, MyBatis 还是需要手工编写 SQL 语句的。恰好由于互联网行业数据量非常巨大,对 SQL 性能有比较苛刻的要求,往往都需要手工编写 SQL 。在此背景下, MyBatis 逐渐流行。

除此之外,MyBatis 是更加简单,更容易上手的框架,但是功能也是相对简陋点。

本篇就演示下,如何在 Spring Boot 框架中快速集成并使用 MyBatis 。

  1. 实例场景
    本篇我们使用 Spring Boot 与 MyBatis ,开发一个商城系统中商品管理模块后端部分。我们依然遵循 Restful 风格,以便团队小伙伴快速理解与接入。

  2. 数据库模块实现
    我们新建数据库 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;

  1. Spring Boot 后端实现

接下来,我们可以开发 Spring Boot 后端项目了,并使用 MyBatis 作为数据持久层框架。

4.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-mybatis ,生成项目后导入 Eclipse 开发环境。

4.2 引入项目依赖
我们引入 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>

4.3 数据源配置
修改 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

4.4 开发数据对象类
开发 goods 表对应的数据对象类 GoodsDo ,代码如下:

实例:

/**
 * 商品类
 */
public class GoodsDo {
	/**
	 * 商品id
	 */
	private Long id;
	/**
	 * 商品名称
	 */
	private String name;
	/**
	 * 商品价格
	 */
	private String price;
	/**
	 * 商品图片
	 */
	private String pic;
	// 省略 get set方法
}

4.5 开发数据访问层
数据访问层直接使用接口实现即可,接口中添加商品的增删改查基本操作。

实例:

/**
 * 商品数据库访问接口
 */
@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();
}

然后,我们修改 Spring Boot 配置类,添加 @MapperScan 注解,扫描数据访问接口所在的包,

实例:

@SpringBootApplication
@MapperScan("com.imooc.springbootmybatis") // 指定MyBatis扫描的包,以便将数据访问接口注册为bean
public class SpringBootMybatisApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootMybatisApplication.class, args);
	}
}

4.6 添加 MyBatis 映射文件
编写数据访问层接口之后,MyBatis 需要知道,如何将接口方法及参数转换为 SQL 语句,以及 SQL 语句执行结果如何转换为对象。这些都是通过映射文件描述的, MyBatis 映射文件就是描述对象 - 关系映射的配置文件。

首先我们通过 application.properties 指定映射文件的位置:

实例:

# 指定MyBatis配置文件位置
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>
  1. 测试
    我们直接编写测试类,对数据访问接口进行测试。此处通过 @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则测试通过
	}
}

测试结果如下,说明所有测试都通过了。

JUnit 测试结果
6. 小结
MyBatis 可以自由的编写 SQL 语句,开发人员可以充分发挥 SQL 语句的性能。

Spring Boot 中使用 MyBatis 操作数据库十分方便,引入相关依赖后,定义数据访问接口,然后通过映射文件描述对象 - 关系映射即可。当然不要忘记通过 MapperScan 注解扫描数据访问接口所在的包,以便发现和注册相关的组件。

MyBatis 还有一些简化开发的工具和框架,如 MyBatis-Plus 、 MyBatis-Generator ,可以简化 MyBatis 开发过程,在一定程度上提高开发效率。感兴趣的同学可以通过网络获取相关资料进一步学习。

Spring Boot 运行管理

Spring Boot 日志管理

  1. 前言
    谁能保证开发的软件系统没有问题?恐怕任何一个有经验的程序员都不敢承诺吧!

在软件的设计、开发阶段,大家都是尽心尽力去做好各项工作,期望能有一个满意的效果。

但是一个投入生产环境、拥有众多用户的软件系统必然是一个复杂的系统工程,不经历现实的检验,没有人能准确地知道它到底会不会有问题。

所以,日志是重要的,不可或缺的。日志是软件系统出现故障时,分析问题的主要依据。就像飞机的黑匣子,平时感觉毫不起眼,到了关键时刻必须要依靠它!

  1. Spring Boot 日志管理
    2.1 默认日志配置
    Spring Boot 默认已经集成了日志功能,使用的是 logback 开源日志系统。

我们新建一个项目,Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-log。生成项目后导入 Eclipse 开发环境,然后运行启动类,可以清楚地看到控制台打印的日志信息。Spring Boot 日志默认级别是 INFO ,下图也输出了几条 INFO 级别的日志。

在这里插入图片描述

Spring Boot 项目启动时控制台输出的内容
Spring Boot 默认的日志输出内容含义如下:

  • 日期时间:精确到毫秒。
  • 日志级别:打印 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 等级别日志信息。
  • 进程 ID:当前项目进程 ID 。
  • 分隔符:— 是分隔符,分隔符后面代表具体的日志内容。
  • 线程名:方括号中间的内容表示线程名称。
  • 类名:当前日志打印所属的类名称。
  • 日志内容:开发人员设定的日志具体内容。
    2.2 日志级别控制
    有时候,我们想指定打印的日志的级别,可以通过配置文件来设置。

实例:

# 设置日志级别
logging.level.root=WARN

上面的配置表示项目日志的记录级别为 WARN ,所以会打印 WARN 及优先级更高的 ERROR 级别的日志。此时我们编写一个测试类,看看具体打印日志的情况。

实例:

@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");
	}
}

运行测试类,控制台打印内容如下,说明我们指定的日志级别生效了。
在这里插入图片描述

Tips: logging.level.root=WARN 中的 root 可以改为指定包名或类名,表示设置指定包或者类的日志级别。

2.3 输出日志文件
控制台日志保存的内容十分有限,大多数情况下我们需要将日志写入文件,便于追溯。

可以通过配置文件指定日志文件,如下配置会将日志打印到 C:\logs\spring-boot-log.log 文件中。

实例:

# 设置日志文件
logging.file=C:\\logs\\spring-boot-log.log

也可以指定日志文件输出的目录, Spring Boot 项目会在指定输出目录下新建 spring.log 文件,并在文件中写入日志。

实例:

# 设置日志目录
logging.path=C:\\logs

Tips:如果同时配置了 logging.file 和 ogging.path ,则只有 logging.file 生效。

2.4 使用 lombok 插件简化日志代码
在上面的示例中,如果要打印日志,需要添加一行代码 private Logger logger = LoggerFactory.getLogger(this.getClass()); 还是比较麻烦的。我们可以安装 lombok 插件,使用一个注解代替这行代码。

2.4.1 下载 lombok 插件
从 lombok 下载链接 下载 lombok 插件。

2.4.2 安装 lombok 插件
双击打开 lombok.jar ,点击 Specify Location 按钮,选择 eclipse.exe ,然后点击 Install 安装插件。
在这里插入图片描述

lombok 插件安装
2.4.3 引入 lombok 依赖
lombok 安装后还需要引入依赖项,在 pom.xml 中添加如下依赖即可。

实例:

		<!-- lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.12</version>
			<scope>provided</scope>
		</dependency>

2.4.4 使用注解输出日志
此时,可以直接给类添加注解,然后就能直接输出日志了。

实例:

@SpringBootTest
@Slf4j // 添加日志输出注解
class LogTest {
	// 不再需要定义 logger
	// private Logger logger = LoggerFactory.getLogger(this.getClass());

	@Test
	void testPrintLog() {
		// 直接使用log输出日志
		log.trace("trace log");
		log.debug("debug log");
		log.info("info log");
		log.warn("warn log");
		log.error("error log");
	}
}

Tips:lombok 插件的功能比较强大,不仅可以简化日志模板代码,还可以自动生成常用的 getter /setter/toString 等模板代码,感兴趣的同学可以查阅相关资料。

  1. 自定义日志配置
    Spring Boot 也支持自定义日志配置,可以直接采用指定日志系统的配置文件,如 logback 、 log4j 。以 logback 为例,可以直接在 application.properties 文件中指定 logback 配置文件。

实例:

# 指定logback配置文件,位于resources目录下
logging.config=classpath:logback-spring.xml

Tips:使用 logback 日志系统后,日志级别与日志文件等信息都可以使用 logback-spring.xml 文件设置,不再需要从
properties 文件中设置了。

在生产环境,我们希望指定日志保存的位置,另外日志不能无限制一直保存,一般情况下保存最近 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 日志系统的功能比较全面,网上可以查询到的资料也非常多,大家可以自行查阅以做进一步的了解。

  1. 小结
    Spring Boot 项目可以使用简单的几个配置,实现日志的打印,并设置相应的级别、日志文件等信息。

如果想要对日志的方方面面进行设定,也可以快速地集成常见的日志系统如 logback 、log4j 。

日志系统对生产环境项目来说是不可或缺的,大家可以选择使用 Spring Boot 集成一种自己用起来顺手的日志系统。

Spring Boot 异常处理

  1. 前言
    程序中出现异常是普遍现象, Java 程序员想必早已习惯,根据控制台输出的异常信息,分析异常产生的原因,然后进行针对性处理的过程。

Spring Boot 项目中,数据持久层、服务层到控制器层都可能抛出异常。如果我们在各层都进行异常处理,程序代码会显得支离破碎,难以理解。

实际上,异常可以从内层向外层不断抛出,最后在控制器层进行统一处理。 Spring Boot 提供了全局性的异常处理机制,本节我们就分别演示下,默认情况、控制器返回视图、控制器返回 JSON 数据三种情况的异常处理方法。

  1. Spring Boot 默认异常处理机制
    Spring Boot 开发的 Web 项目具备默认的异常处理机制,无须编写异常处理相关代码,即可提供默认异常机制,下面具体演示下。

2.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-default ,生成项目后导入 Eclipse 开发环境。

2.2 引入项目依赖
引入 Web 项目依赖即可。

实例:

		<!-- web项目依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

2.3 Spring Boot 默认异常处理
我们在启动项目, Spring Boot Web 项目默认启动端口为 8080 ,所以直接访问 http://127.0.0.1:8080 ,显示如下:
在这里插入图片描述

Spring Boot 默认异常信息提示页面
如上图所示,Spring Boot 默认的异常处理机制生效,当出现异常时会自动转向 /error 路径。

  1. 控制器返回视图时的异常处理
    在使用模板引擎开发 Spring Boot Web 项目时,控制器会返回视图页面。我们使用 Thymeleaf 演示控制器返回视图时的异常处理方式,其他模板引擎处理方式也是相似的。

3.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-controller,生成项目后导入 Eclipse 开发环境。

3.2 引入项目依赖
引入 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>

3.3 定义异常类
在异常处理之前,我们应该根据业务场景具体情况,定义一系列的异常类,习惯性的还会为各种异常分配错误码,如下图为支付宝开放平台的公共错误码信息。
在这里插入图片描述

支付宝开放平台错误码
本节我们为了演示,简单的定义 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 PasswordException extends BaseException {
	public PasswordException() {
		super(10001, "密码错误");
	}
}

实例:


/**
 * 验证码错误异常
 */
public class VerificationCodeException extends BaseException {
	public VerificationCodeException() {
		super(10002, "验证码错误");
	}
}

3.4 控制器抛出异常
定义控制器 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";
	}
}


3.5 开发基于 @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;
	}
}

按照 ControllerExceptionHandler 类的处理逻辑,当发生 BaseException 类型的异常时,会跳转到 myerror.html 页面,并显示相应的错误码和错误信息;当发生其他类型的异常时,错误码为 99999 ,错误信息为相关的异常信息。

3.6 开发前端页面
在 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>

实例:

<!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>

3.7 测试
启动项目,分别访问控制器中的 4 个方法,结果如下:
在这里插入图片描述

访问正常方法 /goods
在这里插入图片描述

访问抛出自定义异常的方法 /checkPassword
在这里插入图片描述

访问抛出自定义异常的方法 /checkVerification
在这里插入图片描述

访问抛出未自定义异常的方法 /other
可见,当控制器方法抛出异常时,会按照全局异常类设定的逻辑统一处理。

  1. 控制器返回 JSON 数据时的异常处理
    在控制器类上添加 @RestController 注解,控制器方法处理完毕后会返回 JSON 格式的数据。

此时,可以使用 @RestControllerAdvice 注解标注的类 ,来捕获 @RestController 标注的控制器抛出的异常。

4.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-restcontroller,生成项目后导入 Eclipse 开发环境。

4.2 引入项目依赖
引入 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>

4.3 定义异常类
还是使用上文中定义的异常类即可。

4.4 统一控制器返回数据格式
这时候,我们就需要思考一个问题了。前端请求后端控制器接口后,怎么区分后端接口是正常返回结果,还是发生了异常?

不论后端接口是正常执行,还是中间发生了异常,最好给前端返回统一的数据格式,便于前端统一分析处理。

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
}

4.5 控制器抛出异常
定义控制器 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);
	}
}

4.6 开发基于 @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);
	}
}

4.7 测试
启动项目,分别尝试访问控制器中的 4 个接口,结果如下。

在这里插入图片描述

访问正常方法 /goods
在这里插入图片描述

访问抛出异常的方法 /checkPassword
在这里插入图片描述

访问抛出异常的方法 /checkVerification
在这里插入图片描述

访问抛出异常的方法 /other
5. 小结
Spring Boot 的默认异常处理机制,实际上只能做到提醒开发者 “这个后端接口不存在” 的作用,作用非常有限。

所以我们在开发 Spring Boot 项目时,需要根据项目的实际情况,定义各类异常,并站在全局的角度统一处理异常。

不管项目有多少层次,所有异常都可以向外抛出,直到控制器层进行集中处理。

  • 对于返回视图的控制器,如果没发生异常就跳转正常页面,如果发生异常可以自定义错误信息页面。
  • 对于返回 JSON 数据的控制器,最好是定义统一的数据返回格式,便于前端根据返回信息进行正常或者异常情况的处理。

Spring Boot 定时任务

  1. 前言
    定时任务绝对是实际项目中的刚需。
  • 我们想监控一个重点服务的运行状态,可以每隔 1 分钟调用下该服务的心跳接口,调用失败时即发出告警信息;
  • 我们想每天凌晨的时候,将所有商品的库存置满,以免早上忘记添加库存影响销售;
  • 我们想在每个周六的某个时段进行打折促销。

在以上的案例中,或者是指定时间间隔,或者是指定时间节点,按设定的任务进行某种操作,这就是定时任务了。

在 Spring Boot 中实现定时任务简单而灵活,本节我们来体验下。

  1. Spring Task 定时任务
    Spring Task 是 Spring Boot 内置的定时任务模块,可以满足大部分的定时任务场景需求。

通过为方法添加一个简单的注解,即可按设定的规则定时执行该方法。

下面就演示下 Spring Boot 中使用 Spring Task 的具体方法。

2.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-task ,生成项目后导入 Eclipse 开发环境。

2.2 开启定时任务
在启动类上添加 @EnableScheduling 注解,开启定时任务功能。

实例:

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class SpringBootTaskApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootTaskApplication.class, args);
	}
}

2.3 通过注解设定定时任务
新建 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);
	}
}

上面例子执行情况如下,可见是每隔 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

实例:

/**
 * 任务类
 */
@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

2.4 使用 Cron 表达式
@Scheduled 也支持使用 Cron 表达式, Cron 表达式可以非常灵活地设置定时任务的执行时间。以本节开头的两个需求为例:

  • 我们想监控一个重点服务的运行状态,可以每隔 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());
	}
}

Cron 表达式并不难理解,从左到右一共 6 个位置,分别代表秒、时、分、日、月、星期,以秒为例:

  • 如果该位置上是 0 ,表示在第 0 秒执行;
  • 如果该位置上是 * ,表示每秒都会执行;
  • 如果该位置上是 ? ,表示该位置的取值不影响定时任务,由于月份中的日和星期可能会发生意义冲突,所以日、 星期中需要有一个配置为 ? 。

按照上面的理解,cron = “0 * * * * ?” 表示在每分钟的 00 秒执行、cron = “0 0 0 * * ?” 表示在每天的 00:00:00 执行。

Tips:Cron 表达式的描述能力很强,此处只是简单提及,感兴趣的同学可以自行查阅相关资料了解更多信息。

  1. Quartz 定时任务
    Spring Task 已经可以满足绝大多数项目对定时任务的需求,但是在企业级应用这个领域,还有更加强大灵活的 Quartz 框架可供选择。

在这里插入图片描述

Quartz 官网介绍:企业级的任务调度框架
举个例子,当我们想根据数据库中的配置,动态地指定商品打折的时间区间时,就可以利用 Quartz 框架来实现。 OK ,接下来我们就来具体完整实现下。

3.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-quartz ,生成项目后导入 Eclipse 开发环境。

3.2 引入项目依赖
需要引入 Quartz 框架相关依赖。

实例:

		<!-- Quartz -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-quartz</artifactId>
		</dependency>

3.3 开启定时任务
同样需要,在启动类上添加 @EnableScheduling 注解,开启定时任务功能。

实例:

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class SpringBootQuartzApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootQuartzApplication.class, args);
	}
}

3.4 Quartz 定时任务开发
Quartz 定时任务需要通过 Job 、 Trigger 、 JobDetail 来设置。

  • Job:具体任务操作类
  • Trigger:触发器,设定执行任务的时间
  • JobDetail:指定触发器执行的具体任务类及方法

我们先开发一个 Job 组件:

实例:

/**
 * 打折任务
 */
@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;
	}
}

具体分析下上面的代码:

  1. 触发器设定的 Corn 表达式为 0 0 8 ? * 7 ,表示每周六的 08:00:00 执行 1 次;

  2. 触发器指定的 JobDetail 为 jobFactoryBean 工厂的一个对象,而 jobFactoryBean 指定的对象及方法为 discountJob 与 execute () ;

  3. 所以每周六的 8 点,就会运行 discountJob 组件的 execute () 方法 1 次;

  4. Corn 表达式和执行任务、方法均以参数形式存在,这就意味着我们完全可以根据文件或数据库配置动态地调整执行时间和执行的任务;

  5. 最后,周六 8 点的时候,商品都打了 5 折,别忘了促销结束的时候恢复价格啊。

  6. 小结
    Spring Boot 可以利用一个简单的注解,快速实现定时任务的功能。

说实话我第一次使用 @Scheduled 注解时,完全被这种开箱即用型的简洁震撼了,我的感受是:似乎不能更加简洁了。

如果感觉 Spring Task 提供的定时任务机制还不足以满足需求,Spring Boot 还可以方便地集成 Quartz 框架来帮忙。

开箱即用满足不了,还可以即插即用,确实够人性化的。

Spring Boot 使用拦截器

  1. 前言

拦截器这个名词定义的非常形象,就像导弹要攻击目标的时候,可能会被先进的反导系统拦截,此处的反导系统就是一种拦截器。

我们开发的应用,对外暴露的是控制器中定义的 API 方法,我们可以在 API 方法的外围放置拦截器,所有对 API 的访问都可以通过拦截器进行过滤。

OK,那么这样的拦截有什么意义吗,其实已经很明显了,反导系统可以保护目标的安全并识别对目标的攻击行为。同理,拦截器可以跟踪对应用的访问行为,对合法访问行为予以放行,对非法访问行为予以拒绝。怎么样,是不是很牛,接下来咱们就在 Spring Boot 项目中具体实现下。

  1. 跟踪访问行为

要想实现对访问的拦截,首先要能跟踪访问行为,我们在 Spring Boot 中引入拦截器来实现下。

2.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-interceptor ,生成项目后导入 Eclipse 开发环境。

2.2 引入项目依赖
引入 Web 项目依赖即可。

实例:

		<!-- web项目依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

2.3 创建拦截器
创建的类实现 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());
	}
}

在上面的实例中,我们定义了一个拦截器类 MyInterceptor ,通过实现 HandlerInterceptor 接口,该类具备了拦截器的功能。

MyInterceptor 中的方法执行顺序为 preHandle – Controller 方法 – postHandle – afterCompletion ,所以拦截器实际上可以对 Controller 方法执行前后进行拦截监控。

最后还有一个非常重要的注意点, preHandle 需要返回布尔类型的值。 preHandle 返回 true 时,对控制器方法的请求才能到达控制器,继而到达 postHandle 和 afterCompletion 方法;如果 preHandle 返回 false ,后面的方法都不会执行。

2.4 配置拦截器
上一步我们开发了配置器类,如果想让配置器生效,还需要通过配置类进行相应配置。

实例:

/**
 * Web配置类
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
	/**
	 * 添加Web项目的拦截器
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 对所有访问路径,都通过MyInterceptor类型的拦截器进行拦截
		registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
	}
}

2.5 创建控制器
我们建立一个简单的控制器,实现登录方法,以便检验拦截器的效果。

实例:

/**
* 登录控制器
*/
@RestController
public class LoginController {
   @RequestMapping("/login")
   public boolean login(String username, String password) {
   	System.out.println(new Date() + " 某用户尝试登录,用户名:" + username + " 密码:" + password);
   	return true;
   }
}

2.6 跟踪访问行为
运行启动类,访问 http://127.0.0.1:8080/login?username=imooc&password=123,控制台输出如下:

控制台输出内容
可见我们已经完整的跟踪了一次对 http://127.0.0.1:8080/login 接口的访问。

  1. 实现访问控制
    区分合法、非法访问,最常见的就是根据用户的登录状态、角色判断。接下来我们就演示下,对未登录用户非法访问请求的拦截。

3.1 修改控制器方法
修改登录方法,当用户输入的用户名和密码正确时,通过 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");
	}
}

3.2 修改拦截器方法
由于用户在登录之前还没有设置 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;
			}
		}
	}
}

3.3 测试
首先直接请求 http://127.0.0.1:8080/info ,由于此时未登录,所以请求被拦截,网页输出如下:
在这里插入图片描述

访问被拦截
如果先请求登录方法 http://127.0.0.1:8080/login?username=imooc&password=123 ,然后访问 http://127.0.0.1:8080/info ,则网页输出:
在这里插入图片描述

登录成功后,访问正常通过拦截器
4. 小结
Spring Boot 的拦截器能够管理对控制器方法的访问请求,通过使用拦截器,可以实现访问控制,加强项目的安全性。

当然对于更加复杂的安全管理续期, Spring Boot 也可以快速的整合 Spring Security 或 Shiro ,以构建企业级的安全管理体系,在后续章节再进一步介绍吧。

Spring Boot 应用场景

Spring Boot AOP 应用场景

  1. 前言
    Spring 最重要的两个功能,就是依赖注入(DI)和面向切面编程 (AOP)。

AOP 为我们提供了处理问题的全局化视角,使用得当可以极大提高编程效率。

Spring Boot 中使用 AOP 与 Spring 中使用 AOP 几乎没有什么区别,只是建议尽量使用 Java 配置代替 XML 配置。

本节就来演示下 Spring Boot 中使用 AOP 的常见应用场景。

  1. 构建项目
    首先我们需要构建一个 Spring Boot 项目并引入 AOP 依赖,后续场景演示均是在这个项目上实现的。

2.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-aop,生成项目后导入 Eclipse 开发环境。

2.2 引入项目依赖
我们引入 Web 项目依赖与 AOP 依赖。

实例:

		<!-- Web项目依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- AOP -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

2.3 新建控制层、服务层、数据访问层
为了便于后续的演示,我们依次新建控制类、服务类、数据访问类,并将其放入对应的包中,项目结构如下:

在这里插入图片描述

项目结构
各个类代码如下,注意此处仅仅是为了演示 AOP 的使用,并未真实访问数据库,而是直接返回了测试数据。

实例:

/**
 * 商品控制器类
 */
@RestController
public class GoodsController {
	@Autowired
	private GoodsService goodsService;

	/**
	 * 获取商品列表
	 */
	@GetMapping("/goods")
	public List getList() {
		return goodsService.getList();
	}
}

实例:

/**
 * 商品服务类
 */
@Service
public class GoodsService {
	@Autowired
	private GoodsDao goodsDao;

	/**
	 * 获取商品信息列表
	 */
	public List getList() {
		return goodsDao.getList();
	}
}

实例:

/**
 * 商品数据库访问类
 */
@Repository // 标注数据访问类
public class GoodsDao {
	/**
	 * 查询商品列表
	 */
	public List getList() {
		return new ArrayList();
	}
}

  1. 使用 AOP 记录日志
    如果要记录对控制器接口的访问日志,可以定义一个切面,切入点即为控制器中的接口方法,然后通过前置通知来打印日志。

实例:

/**
 * 日志切面
 */
@Component
@Aspect // 标注为切面
public class LogAspect {
	private Logger logger = LoggerFactory.getLogger(this.getClass());

	// 切入点表达式,表示切入点为控制器包中的所有方法
	@Pointcut("within(com.imooc.springbootaop.controller..*)")
	public void LogAspect() {
	}

	// 切入点之前执行
	@Before("LogAspect()")
	public void doBefore(JoinPoint joinPoint) {
		logger.info("访问时间:{}--访问接口:{}", new Date(), joinPoint.getSignature());
	}
}

启动项目后,访问控制器中的方法之前会先执行 doBefore 方法。控制台打印如下:

2020-05-25 22:14:12.317  INFO 9992 --- [nio-8080-exec-2] com.imooc.springbootaop.LogAspect        :
访问时间:Mon May 25 22:14:12 CST 2020--访问接口:List com.imooc.springbootaop.controller.GoodsController.getList()

  1. 使用 AOP 监控性能
    在研发项目的性能测试阶段,或者项目部署后,我们会希望查看服务层方法执行的时间。以便精准的了解项目中哪些服务方法执行速度慢,后续可以针对性的进行性能优化。

此时我们就可以使用 AOP 的环绕通知,监控服务方法的执行时间。

实例:

/**
 * 服务层方法切面
 */
@Component
@Aspect // 标注为切面
public class ServiceAspect {
	private Logger logger = LoggerFactory.getLogger(this.getClass());

	// 切入点表达式,表示切入点为服务层包中的所有方法
	@Pointcut("within(com.imooc.springbootaop.service..*)")
	public void ServiceAspect() {
	}

	@Around("ServiceAspect()") // 环绕通知
	public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
		long startTime = System.currentTimeMillis();// 记录开始时间
		Object result = joinPoint.proceed();
		logger.info("服务层方法:{}--执行时间:{}毫秒", joinPoint.getSignature(), System.currentTimeMillis() - startTime);
		return result;
	}
}

当服务层方法被调用时,控制台输入日志如下:

Tips:正常情况下,用户查看页面或进行更新操作时,耗时超过 1.5 秒,就会感觉到明显的迟滞感。由于前后端交互也需要耗时,按正态分布的话,大部分交互耗时在 0.4秒 左右。所以在我参与的项目中,会对耗时超过 1.1 秒的服务层方法进行跟踪分析,通过优化 SQL 语句、优化算法、添加缓存等方式缩短方法执行时间。上面的数值均为我个人的经验参考值,还要视乎具体的服务器、网络、应用场景来确定合理的监控临界值。

  1. 使用 AOP 统一后端返回值格式
    前后端分离的项目结构中,前端通过 Ajax 请求后端接口,此时最好使用统一的返回值格式供前端处理。此处就可以借助 AOP 来实现正常情况、异常情况返回值的格式统一。

5.1 定义返回值类
首先定义返回值类,它属于业务逻辑对象 (Bussiness Object),所以此处命名为 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();
	}
	// 省略 get set
}

5.2 修改控制层返回值类型
对所有的控制层方法进行修改,保证返回值均通过 ResultBo 包装,另外我们再定义一个方法,模拟抛出异常的控制层方法。

实例:

	/**
	 * 获取商品列表
	 */
	@GetMapping("/goods")
	public ResultBo getList() {
		return new ResultBo(goodsService.getList());
	}
	/**
	 * 模拟抛出异常的方法
	 */
	@GetMapping("/test")
	public ResultBo test() {
		int a = 1 / 0;
		return new ResultBo(goodsService.getList());
	}

5.3 定义切面处理异常返回值
正常控制层方法都返回 ResultBo 类型对象,然后我们需要定义切面,处理控制层抛出的异常。当发生异常时,同样返回 ResultBo 类型的对象,并且对象中包含异常信息。

实例:

/**
 * 返回值切面
 */
@Component
@Aspect
public class ResultAspect {
	// 切入点表达式,表示切入点为返回类型ResultBo的所有方法
	@Pointcut("execution(public com.imooc.springbootaop.ResultBo *(..))")
	public void ResultAspect() {
	}

	// 环绕通知
	@Around("ResultAspect()")
	public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
		try {
			return joinPoint.proceed();// 返回正常结果
		} catch (Exception ex) {
			return new ResultBo<>(ex);// 被切入的方法执行异常时,返回ResultBo
		}
	}
}

5.4 测试
启动项目,访问 http://127.0.0.1:8080/goods 返回数据如下:

实例:

{"code":0,"msg":"操作成功","data":[]}

然后访问 http://127.0.0.1:8080/test ,返回数据如下:

实例:

{"code":99999,"msg":"/ by zero","data":null}

这样,前端可以根据返回值的 code, 来判断后端是否正常响应。如果 code 为 0 ,则进行正常业务逻辑操作;如果 code 非 0 ,则可以弹窗显示 msg 提示信息。

  1. 小结
    AOP 之所以如此重要,在于它提供了解决问题的新视角。通过将业务逻辑抽象出切面,功能代码可以切入指定位置,从而消除重复的模板代码。

使用 AOP 有一种掌握全局的快感,发现业务逻辑中的切面颇有一番趣味,希望大家都能多多体会,编程且快乐着应该是我辈的追求。

Spring Boot Redis 应用场景

  1. 前言
    Redis 其实就是基于内存的键值型数据库,与 Oracle 、 SQL Server 、 MySQL 等传统关系型数据库相比,它最大的优势就是读写速度快。

到底有多快呢,我曾经使用 Windows 版本的 Redis 进行过真实测试,每秒读写次数均可以超过1 万次。据了解 Redis 每秒的读写操作次数其实是可以达到 10 万多次的。

所以 Redis 非常适合作为热点数据的缓存,这个我们在上一节已经演示过了。本节通过其他两个实际场景来演示下 Spring Boot 中如何应用 Redis 。

  • 网站的访问次数
  • 热门商品排行榜
  1. 网站的访问次数
    大型网站访问次数的查询、更新非常频繁,如果通过关系数据库读写,无疑会耗费大量的性能,而使用 Redis 可以大幅提高速度并降低对关系数据库的消耗。

2.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-redis,生成项目后导入 Eclipse 开发环境。

2.2 引入项目依赖
我们引入 Web 项目依赖与 Redis 依赖。

实例:

		<!-- Web 依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Redis 依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

2.3 配置 Redis 数据库连接
修改 application.properties 配置文件内容如下。

实例:

# Redis库的编号
spring.redis.database=0
# Redis实例地址
spring.redis.host=127.0.0.1
# Redis实例端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=Easy@0122
# Redis连接池最大连接数
spring.redis.jedis.pool.max-active=10
# Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=10
# Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0

2.4 开发网站访问统计服务类
开发网站访问统计服务类,在第 1 次获取访问次数时初始化次数为 0 ,后续每次访问次数加 1 。

实例:

/**
 * 网站访问统计服务类
 */
@Service
public class VisitService {
	// 设定访问次数Redis键名
	private final static String KEY = "visit_count";

	// 注入redisTemplate操作Redis
	@Autowired
	private RedisTemplate<String, String> redisTemplate;

	// 获取当前访问次数
	public String getCurrentCount() {
		String count = redisTemplate.opsForValue().get(KEY);
		if (count == null || "".equals(count)) {
			redisTemplate.opsForValue().set(KEY, "0");
			return "0";
		}
		return count;
	}

	// 访问次数加1
	public void addCount() {
		redisTemplate.opsForValue().increment(KEY, 1);
	}
}

2.5 并发访问测试
我们通过测试类发起并发访问测试,代码如下:

实例:

/**
 * 访问统计服务测试
 */
@SpringBootTest
class VisitServiceTest {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	@Autowired
	private VisitService visitService;

	@Test
	void test() {
		logger.info("访问次数:{}", visitService.getCurrentCount());
		// 使用线程池快速发起10000次访问
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 10000; i++) {
			cachedThreadPool.execute(new Runnable() {
				public void run() {
					visitService.addCount();
				}
			});
		}
	}
}

此时我们通过 Redis 客户端发现 visit_count 的值如下:

并发访问测试结果
< Tips:Redis 中的操作都是原子性的,要么执行,要么不执行,在高并发场景下依然可以准确的进行计数,关键是速度还非常之快!

  1. 热门商品排行榜
    如果是大型网站,时刻有很多用户在访问网页,对热门商品排行榜的访问频率是非常恐怖的。

我们可以通过定时器,定时从关系数据库中取出热门商品数据放入 Redis 缓存,用户访问网页时,直接从缓存中获取热门商品数据。这将大大提高响应速度,并降低对关系数据库的性能损耗。

3.1 定义商品类
我们简单的定义一个商品类,便于展现商品排行榜数据。

实例:

/**
 * 商品类
 */
public class GoodsDo {
	/**
	 * 商品id
	 */
	private Long id;
	/**
	 * 商品名称
	 */
	private String name;
	/**
	 * 商品价格
	 */
	private String price;
	/**
	 * 商品图片
	 */
	private String pic;
	// 省略get set方法
}

3.2 开发商品排行榜服务类
开发商品排行榜服务类,负责从数据库查询最新排行榜信息,并更新到 Redis ,以及从 Redis 中取出排行榜信息。

实例:

/**
 * 商品排行榜服务类
 */
@Service
public class GoodsRankService {
	// 设定商品排行榜Redis键名
	private final static String KEY = "goods_rank";

	// 注入redisTemplate操作Redis
	@Autowired
	private RedisTemplate<String, String> redisTemplate;

	// 更新Redis缓存的排行榜
	public void updateRankList() throws JsonProcessingException {
		// 此处直接定义商品排行榜,真实场景应为从数据库获取
		List<GoodsDo> rankList = new ArrayList<GoodsDo>();
		GoodsDo goods = new GoodsDo();
		goods.setId(1L);
		goods.setName("鸡蛋" + new Date());// 添加时间信息,以便测试缓存更新了
		rankList.add(goods);
		// 将rankList序列化后写入Reidis
		ObjectMapper mapper = new ObjectMapper();
		redisTemplate.opsForValue().set(KEY, mapper.writeValueAsString(rankList));
	}

	// 获取Redis缓存的排行榜
	public List<GoodsDo> getRandkList() throws JsonMappingException, JsonProcessingException {
		ObjectMapper mapper = new ObjectMapper();
		return mapper.readValue(redisTemplate.opsForValue().get(KEY), List.class);
	}
}

3.3 通过定时器更新排行榜
为启动类添加 @EnableScheduling 注解,以便开启定时任务,然后编写 RankListUpdateTask 类定时刷新排行榜。

实例:

/**
 * 排行榜更新任务
 */
@Component
public class RankListUpdateTask {
	@Autowired
	private GoodsRankService goodsRankService;

	/**
	 * 容器启动后马上执行,且每1秒执行1次
	 */
	@Scheduled(initialDelay = 0, fixedRate = 1000)
	public void execute() throws InterruptedException, JsonProcessingException {
		goodsRankService.updateRankList();
	}
}

3.4 开发控制器方法
我们还需要一个控制器方法,用于演示获取商品列表的结果。

实例:

@RestController
public class GoodsRankController {
	@Autowired
	private GoodsRankService goodsRankService;

	@GetMapping("getRankList")
	public List getRankList() throws Exception {
		return goodsRankService.getRandkList();
	}
}

3.5 测试
运行启动类,然后访问 http://127.0.0.1:8080/getRankList ,结果如下:

[{"id":1,"name":"鸡蛋Thu May 28 22:47:33 CST 2020","price":null,"pic":null}]

稍等会再次访问,结果如下:

[{"id":1,"name":"鸡蛋Thu May 28 22:48:09 CST 2020","price":null,"pic":null}]

说明我们设计的缓存机制生效了。

  1. 小结
    开发的项目多了,越来越能体会,传统数据库访问速度是限制系统性能的最大瓶颈。

而 Redis 基于内存的特性,可以极大地提高读写效率,使用得当,往往使系统性能有质的提升。

Spring Boot 可以非常方便地集成 Redis ,当我们在项目开发中遇到访问频率非常高的热点数据时,可以优先考虑使用 Redis 进行存储操作。

Spring Boot RabbitMQ 应用场景

  1. 前言
    消息队列是一个容器,可以对程序产生的消息进行存储。消息队列的主要用途是削峰、异步、解耦,我们用一个实际场景来解释下。

有一家果汁生产企业,张三是采购员,负责采购水果;李四、赵五是配送员,分别负责将苹果、香蕉配送到生产车间。

1.1 削峰
传统模式下,张三采购完成,回到公司后,联系李四、赵五配送采购的水果。但是随着公司业务量大增,张三一次性采购的水果,李四、赵五得需要几天才能配送完。所以需要一个仓库,张三采购完成直接放到仓库里,李四、赵五慢慢从仓库取出配送。

此处的仓库就是消息队列,张三是采购消息的生产者,李四、赵五是消费者。当生产的消息太多时,可以使用队列削峰,这样消费者可以慢慢处理消息。

1.2 异步
传统模式下,张三采购完成后,需要等待李四、赵五来取,实际上极大浪费了张三的时间。如果直接放入仓库,可以不必等待,直接进行下面的工作。也就是说,张三与李四、赵五的工作是异步的,减少了等待时间。

1.3 解耦
之前张三采购完成后,有责任通知李四、赵五来取。万一李四、赵五忘带手机,张三还得联系领导协调处理,说实话张三就是个大老粗,整天为这些破事烦得不行。

如果直接放入仓库,张三根本不用管李四、赵五的事情,感觉愉快极了。张三与李四、赵五的工作不再互相依赖,都变得更加简单了,这就是解耦。

  1. RabbitMQ 简介
    RabbitMQ 是非常出名的消息中间件,遵循 AMQP 协议,可以跨平台、跨语言使用。 RabbitMQ 具备低时延、高可用的特点,还有简洁易用的可视化管理界面,所以本节我们使用 RabbitMQ 来进行消息队列技术的演示。
    在这里插入图片描述

RabbitMQ 可视化管理界面
3. Spring Boot 实现
我们就针对上面的场景,使用 Spring Boot ,结合 RabbitMQ 来具体实现下水果采购、配送的管理。

3.1 使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-rabbitmq,生成项目后导入 Eclipse 开发环境。

3.2 引入项目依赖
我们引入 Web 项目依赖与 AMQP 消息队列依赖。

实例:

		<!-- Web 依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- AMQP 依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

3.3 配置 RabbitMQ 连接信息
项目创建后,通过 applicaiton.properties 配置 RabbitMQ 的链接信息。

实例:

#地址
spring.rabbitmq.host=127.0.0.1
#端口 默认5672
spring.rabbitmq.port=5672
#用户名
spring.rabbitmq.username=guest
#密码
sprng.rabbitmq.password=guest

3.4 配置队列
首先配置两个队列,存储苹果采购消息、香蕉采购消息。

实例:

/**
 * 消息队列配置类
 */
@Configuration
public class RabbitConfig {
	/**
	 * 苹果采购消息队列
	 */
	@Bean
	public Queue appleQueue() {
		return new Queue("apple-queue");
	}

	/**
	 * 香蕉采购消息队列
	 */
	@Bean
	public Queue bananaQueue() {
		return new Queue("banana-queue");
	}
}

3.5 配置交换机和绑定
如果消息直接发到队列的话,不够灵活, RabbitMQ 提供了交换机与绑定机制。

消息发送给交换机,交换机可以灵活地与队列进行绑定,这样消息就可以通过多种方式进入队列了。

实例:

	/**
	 * 配置交换机
	 */
	@Bean
	TopicExchange exchangeTopic() {
		return new TopicExchange("exchange-topic");
	}

	/**
	 * 交换机绑定苹果采购消息队列
	 */
	@Bean
	Binding bindAppleQueue() {
		return BindingBuilder.bind(appleQueue()).to(exchangeTopic()).with("#.apple.#");
	}

	/**
	 * 交换机绑定香蕉采购消息队列
	 */
	@Bean
	Binding bindBananaQueue() {
		return BindingBuilder.bind(bananaQueue()).to(exchangeTopic()).with("#.banana.#");
	}

我们来详细解释下交换机与绑定的运行机制。

  • 我们配置了一个交换机 exchangeTopic ,它可以接收消息。
  • 交换机 exchangeTopic 绑定了两个队列,分别是 appleQueue 和 bananaQueue ,说明这两个队列在关注该交换机收到的消息。
  • 那么交换机 exchangeTopic 收到的消息到底会进入哪个队列呢,我们发现交换机的类型是 TopicExchange ,说明该交换机是话题交换机,队列应该是获取其感兴趣的话题相关的消息。
  • 当 appleQueue 队列绑定到交换机时,with(“#.apple.#”) 就表示 appleQueue 关心的是 apple 相关的话题;而 bananaQueue 关心的是 banana 相关的话题。
  • 所以可以推断出,消息在发送时,可以指定话题相关的信息,以便消息能被关注该话题的队列接收。

经过上面的分析,我们就知道了消息发送时通过携带话题信息,交换机会将该消息路由到关心该话题的队列中。

3.6 创建消费者
接下来,我们就可以定义消息的消费者李四、赵五了。他俩分别关心苹果采购消息和香蕉采购消息。也就是监听苹果消息队列和香蕉消息队列。

实例:

/**
 * 消息队列接收
 */
@Component
public class RabbitReceiver {
	/**
	 * lisi负责监听apple-queue
	 */
	@RabbitListener(queues = "apple-queue")
	public void lisi(String msg) {
		System.out.println("李四知道:" + msg);
	}

	/**
	 * zhaowu负责监听banana-queue
	 */
	@RabbitListener(queues = "banana-queue")
	public void zhaowu(String msg) {
		System.out.println("赵五知道:" + msg);
	}
}

3.7 测试
运行启动类,从 RabbitMQ 管理界面可以看到已生成指定名称的队列了。
在这里插入图片描述

RabbitMQ 已生成队列
此时我们定义一个控制器用于发起测试,直接使用 rabbitTemplate 发送消息即可。

实例:

@RestController
public class TestController {
	@Autowired
	private RabbitTemplate rabbitTemplate;

	@GetMapping("/test")
	public void test() {
		// 发送消息 参数分别为:交换机名称、路由键、消息内容
		rabbitTemplate.convertAndSend("exchange-topic", "apple", "苹果来了10斤");
		rabbitTemplate.convertAndSend("exchange-topic", "banana", "香蕉来了5斤");
		rabbitTemplate.convertAndSend("exchange-topic", "apple.banana", "苹果来了8斤;香蕉来了20斤");
	}
}

convertAndSend() 方法的第 1 个参数表示交换机,第 2 个参数表示路由键(消息的话题),第 3 个是消息内容。

所以第 1 个消息会被 apple-queue 接收,第 2 个消息会被 banana-queue 接收,第 3 个消息会被两个队列接收。

我们启动项目,然后访问 http://127.0.0.1:8080/test ,控制台输出如下,验证成功。

赵五知道:香蕉来了5斤
李四知道:苹果来了10斤
赵五知道:苹果来了8;香蕉来了20斤
李四知道:苹果来了8;香蕉来了20
  1. 小结
    本小节通过一个实际应用场景,演示了 Spring Boot 中使用 RabbitMQ 消息队列的方法。

至此, Spring Boot 的内容就全部结束了。纸上得来终觉浅,绝知此事要躬行。任何的实用技能都需要在不断练习与使用中感悟、完善、提升, Spring Boot 也不例外。

所以还没有使用 Spring Boot 的朋友,抓紧上手吧!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值