一、SpringBoot概述
1、springboot介绍
- SpringBoot提供了一种快速使用 Spring 的方式 ,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。 2014 年 4 月, Spring Boot 1.0.0 发布。 Spring 的顶级项目之一(https://spring.io)。
2、Spring缺点
Spring Boot并不是对 Spring 功能上的增强,而是提供了一种快速使用 Spring 的方式。
-
配置繁琐
- 虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的。一开始, Spring 用 XML 配置,而且是很多XML 配置。 Spring 2.5 引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式 XML 配置。Spring 3.0 引入了基于 Java 的配置,这是一种类型安全的可重构配置方式,可以代替 XML 。
- 所有这些配置都代表了开发时的损耗。因为在思考Spring 特性配置和解决业务问题之间需要进行思维切换,所以编写配置挤占了编写应用程序逻辑的时间。和所有框架一样, Spring 实用,但它要求的回报也不少。
-
依赖繁琐
- 项目的依赖管理也是一件耗时耗力的事情。在环境搭建时,需要分析要导入哪些库的坐标,而且还需要分析导入与之有依赖关系的其他库的坐标,一旦选错了依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。
3、SpringBoot功能
-
自动配置
- Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring 配置应该用哪个,不该用哪个。该过程是 SpringBoot 自动完成的。
-
起步依赖
- 起步依赖本质上是一个Maven 项目对象模型( Project Object Model POM ),定义了对其他库的 传递依赖,这些东西加在一起即支持某项功能。
- 简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。
-
辅助功能
- 提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等。
4、SpringBoot快速入门
1. 案例需求
- 搭建 SpringBoot 工程,定义 HelloController.hello 方法,返回 “Hello SpringBoot!” 。
2. 实现步骤
2.1 创建项目
- 新建 Spring Initializr
- 类型:Maven
- 语言:java
- 打包方式:jar
- 添加spring web 依赖
2.2 起步依赖
<?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>
<!--springboot工程需要继承的父工程:主要是管理项目的资源过滤及插件!-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>erer.springboot</groupId>
<artifactId>SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- starter 启动器 -->
<!-- web开发的起步依赖:
实现HTTP接口(该依赖包含Spring NVC)
即:使用 Spring MVC 构建 Web(包括RESTful)应用程序
使用 Tomcat 作为默认嵌入式容器。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- YAML文件编写提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.4</version>
</plugin>
</plugins>
</build>
</project>
2.3 定义 Controller
package erer.springboot.controller;
@RestController
@RequestMapping("/controller")
public class HelloController {
// 请求地址 http://localhost:8080/controller/hello
@RequestMapping("/hello")
public String hello() {
return "hello SpringBoot!";
}
}
2.4 编写引导类
package erer.springboot.springboot;
// 标注这个类是 springboot 的应用
@SpringBootApplication
public class Application {
public static void main(String[] args) {
/*
将 SpringBoot 应用启动,主要做了以下四件事情:
1、推断应用的类型是普通的项目还是Web项目
2、查找并加载所有可用初始化器 , 设置到initializers属性中
3、找出所有的应用程序监听器,设置到listeners属性中
4、推断并设置main方法的定义类,找到运行的主类
*/
SpringApplication.run(Application.class, args);
}
}
2.5 启动测试
- 运行引导类
3. 起步依赖原理分析
- 在 spring boot starter parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。
- 在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。
- 我们的工程继承 parent ,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突等问题。
二、SpringBoot配置
1、配置文件分类
SpringBoot是基于约定的,所以很多配置都有默认值,但如果想使用自己的配置替换默认配置的话,就可以使用application.properties 或者 application.yml application.yaml )进行配置。
- SpringBoot 提供了 2 种配置文件类型: properteis 和 yml/yaml
- 默认配置文件名称: application
- 在同一级目录下优先级为: properties > yml > yaml
1. application.properties
server.port=8080
2. application.yml
server:
port: 8080
2、YAML
- YAML全称是 YAML Ain t Markup Language 。 YAML 是一种直观的能够被电脑识别的的数据数据序列化格式,并且容易被人类阅读,容易和脚本语言交互的,可以被支持 YAML 库的不同的编程语言程序导入,比如: C/C++, Ruby, Python, Java, Perl, C#, PHP等。
- YML 文件是以数据为核心的,比传统的 xml 方式更加简洁。
- YA ML 文件的扩展名可以使用 .yml 或者 .yaml
1. 基本语法
- 大小写敏感
- 数据值前边必须有空格,作为分隔符
- 使用缩进表示层级关系
- 缩进时不允许使用 Tab 键,只允许使用空格 (各个系统 Tab 对应的 空格数目可能不同,导致层次混乱)
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
- # 表示注释,从这个字符一直到行尾,都会被解析器忽略
- 支持松散绑定:first-name — > firstName
2. 数据格式
- 对象(map):键值对的集合
person01:
name: zhangsan
age: 18
# 行内写法
person02: {name: lisi, age: 20}
- 数组:一组按次序排列的值
address01:
- beijing
- shanghai
# 行内写法
address02: [beijing,shanghai]
- 纯量(常量):单个的、不可再分的值
msg01: 'hello \n world' # 单引忽略转义字符,原样输出
msg02: "hello \n world" # 双引识别转义字符,输出回车
- 参数引用
name: wangwu
person:
name: ${name} # 引用上边定义的 name 值
3、读取配置内容
str: abc
person:
name: zhangsan
age: 18
address:
- beijing
- shanghai
msg01: 'hello \n world' # 单引忽略转义字符
msg02: "hello \n world" # 双引识别转义字符
1. @Value
/*
当 测试类 与 启动类 不在同一包 下时需要配置 classes:@SpringBootTest(classes = 启动类.class)
*/
@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Value("${str}")
private String name;
@Value("${person.age}")
private int age;
@Value("${address[0]}")
private String address;
@Value("${msg01}")
private String msg01;
@Value("${msg02}")
private String msg02;
@Test
void contextLoads() {
System.out.println(name); //abc
System.out.println(age); //18
System.out.println(address); //beijing
System.out.println(msg01); //hello \n world
System.out.println(msg02);
/**
hello
world
*/
}
}
2. Environment
import org.springframework.core.env.Environment;
@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Autowired
private Environment environment;
@Test
void contextLoads() {
System.out.println(environment.getProperty("str"));
System.out.println(environment.getProperty("person.age"));
System.out.println(environment.getProperty("address[0]"));
System.out.println(environment.getProperty("msg01"));
System.out.println(environment.getProperty("msg02"));
}
}
3. @ConfigurationProperties
package erer.springboot.springboot.domain;
// 使用在类上用于实例化Bean
@Component
// @ConfigurationProperties
@ConfigurationProperties(prefix = "person")
// 数据校验
@Validated
public class Person {
private String name;
private int age;
private String[] address;
}
@SpringBootTest(classes = Application.class)
class ApplicationTests0 {
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person); //Person{name='zhangsan', age=18, address=[beijing, shanghai]}
}
}
4、JSR303校验
1. 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
2. 使用方法
package erer.springboot.springboot.domain;
// 使用在类上用于实例化Bean
@Component
// 开启数据校验
@Validated
public class Person {
private String name;
// 数据范围校验
@Max(value = 8888, message = "最大值不能超高8888")
@Min(value = 2222, message = "最大值不能小于2222")
private int age;
private String[] address;
@Email // 校验数据是否为邮箱地址
private String email;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize dataSize;
}
- Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)
- DataSize:表示存储空间,可以通过@DataSizeUnit注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)
5、配置文件
- 我们在开发 Spring Boot 应用时,通常同一套程序会被安装到不同环境,比如:开发、测试、生产等。其中数据库地址、服务器端口等等配置都不同,如果每次打包时,都要修改配置文件,那么非常麻烦,profile 功能就是来进行动态配置切换的。
1. profile 配置方式
1.1 多 profile 文件方式
- application.properties
spring.profiles.active=test
- application-dev.properties
server.port=8081
- application-pro.properties
server.port=8082
- application-test.properties
server.port=8083
1.2 yml 多文档方式
server:
port: 8081
spring:
config:
activate:
on-profile: dev
---
server:
port: 8082
spring:
config:
activate:
on-profile: pro
---
server:
port: 8083
spring:
config:
activate:
on-profile: test
---
spring:
profiles:
active: dev
2. profile 激活方式
2.1 配置文件
spring.profiles.active=dev
2.2 虚拟机参数
-Dspring.profiles.active=test
2.3 命令行参数
--spring.profiles.active=dev
6、内部配置加载顺序
Springboot 程序启动时,会从以下位置加载配置文件(加载顺序为下文的排列顺序,高优先级配置的属性会生效):
- file:./config/ 当前项目所在目录下 config 目录的配置文件
- file:./ 当前项目所在目录的配置文件
- classpath:/config/ classpath 下 config 目录的配置文件
- classpath:/ classpath 根目录的配置文件
7、外部配置加载顺序
- 加载指定位置配置文件
--spring.cofig.location=配置文件路径
- 当前 jar 包所在目录下的 config 目录的配置文件
- 当前 jar 包所在目录下的配置文件
通过官网查看外部属性加载顺序:https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
三、数据层解决方案
1、案技术选型
2、内嵌的数据源对象Hikari
1. 引入依赖
<!-- Mysql:数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Mybatis:操作数据库 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
2. 配置数据源
SpringBoot提供了3种内嵌的数据源对象供开发者选择:
- HikariCP:默认内置数据源对象
# 方案一
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///springboot?serverTimezone=UTC
username: root
password: 123456
hikari:
maximum-pool-size: 50
# 方案二
spring:
datasource:
url: jdbc:mysql:///springboot?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
maximum-pool-size: 50
- Tomcat提供DataSource:HikariCP不可用,且在web环境中,将使用tomcat服务器配置的数据源对象
- Commons DBCP:Hikari不可用,tomcat数据源也不可用,将使用dbcp数据源
3. 测试代码
@SpringBootTest
class _DataSource连接 {
@Autowired
private DataSource dataSource;
@Test
void test() {
System.out.println(dataSource);
}
}
3、内置持久化JdbcTemplate
1. 引入依赖
<!-- Mysql:数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- jdbc:操作数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2. 配置数据源
spring:
datasource:
url: jdbc:mysql:///springboot?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
jdbc:
template:
query-timeout: -1 # 查询超时时间
max-rows: 500 # 最大行数
fetch-size: -1 # 缓存行数
3. 测试代码
@SpringBootTest
class _JdbcTemplate测试 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void test() {
String sql = "select * from User";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : list) {
System.out.println(map);
}
System.out.println("--------------------------------------------");
RowMapper<User> rm = (resultSet, i) -> {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
user.setVipLevel(resultSet.getString("vipLevel"));
return user;
};
List<User> userList = jdbcTemplate.query(sql, rm);
for (User user : userList) {
System.out.println(user);
}
System.out.println("--------------------------------------------");
userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
for (User user : userList) {
System.out.println(user);
}
}
}
4、内嵌数据库H2
SpringBoot提供了3种内嵌数据库供开发者选择,提高开发测试效率:
- H2
- HSQL
- Derby
1. 引入依赖
<!-- jdbc:操作数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- H2数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
2. 配置参数
spring:
# H2 配置
h2:
console:
path: /h2
enabled: true
datasource:
url: jdbc:h2:~/test
driver-class-name: org.h2.Driver
username: sa
password: 123456
3. 测试代码
- 测试需要关闭 spring 程序
@SpringBootTest
class _H2测试 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void test() {
String sql = "select * from User";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : list) {
System.out.println(map);
}
System.out.println("--------------------------------------------");
RowMapper<User> rm = (resultSet, i) -> {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
user.setVipLevel(resultSet.getString("vipLevel"));
return user;
};
List<User> userList = jdbcTemplate.query(sql, rm);
for (User user : userList) {
System.out.println(user);
}
System.out.println("--------------------------------------------");
userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
for (User user : userList) {
System.out.println(user);
}
}
}
5、Redis
Redis是一款key-value存储结构的内存级NoSQL数据库:
- 支持多种数据存储格式
- 支持持久化
- 支持集群
1. 引入依赖
<!-- redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置参数
spring:
redis:
host: 127.0.0.1 # redis 的主机 ip
port: 6379
3. 测试代码
- 存取字符串
@SpringBootTest
class _RedisTest {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
void test() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("age", "20");
String age = ops.get("age");
System.out.println(age);
}
}
- 存取对象
package erer.springboot.redis.domain;
@Data
@NoArgsConstructor
@AllArgsConstructor
// 序列化
public class User implements Serializable {
private String name;
private int age;
}
@SpringBootTest
public class _RedisTest {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test01(){
User user = new User("林青霞", 30);
ValueOperations<Object, Object> ops = redisTemplate.opsForValue();
ops.set("user", user);
System.out.println(ops.get("user")); // User(name=林青霞, age=30)
/*
Redis 查询结果:
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04user"
*/
}
@Test
void test02() throws JsonProcessingException {
User user = new User("林青霞", 30);
// User 对象 序列化
String jsonUser = new ObjectMapper().writeValueAsString(user);
ValueOperations<Object, Object> ops = redisTemplate.opsForValue();
ops.set("user", jsonUser);
System.out.println(ops.get("user")); // {"name":"林青霞","age":30}
/*
Redis 查询结果:
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04user"
*/
}
@Test
void test03() throws JsonProcessingException {
User user = new User("林青霞", 30);
// User 对象 序列化
String jsonUser = new ObjectMapper().writeValueAsString(user);
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("user", jsonUser);
System.out.println(ops.get("user")); // {"name":"林青霞","age":30}
/*
Redis 查询结果:
127.0.0.1:6379> keys *
1) "user"
*/
}
}
4. 自定义 RedisUtil
4.1 自定义 RedisTemplate
package erer.springboot.redis.config;
//RedisConnectionFactory factory
@Configuration
public class RedisConfig {
// 自定义 RedisTemplate
@Bean
// 如果方法名为 redisTemplate,RedisTemplate<Object, Object>将无法使用
public RedisTemplate<String, Object> myRedisTemplate(LettuceConnectionFactory factory) {
// 方便开发,一般使用 RedisTemplate<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Json 序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的 序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4.2 自定义 RedisUtil
package erer.springboot.redis.utils;
@Component
public final class RedisUtil {
@Resource
// 变量名称要与 bean 一致
private RedisTemplate<String, Object> myRedisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
myRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return myRedisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return myRedisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
myRedisTemplate.delete(key[0]);
} else {
Collection<?> list = CollectionUtils.arrayToList(key);
myRedisTemplate.delete((Collection<String>) list);
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : myRedisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
myRedisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
myRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return myRedisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return myRedisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return myRedisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return myRedisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
myRedisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
myRedisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
myRedisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
myRedisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
myRedisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return myRedisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return myRedisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return myRedisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return myRedisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return myRedisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return myRedisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = myRedisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return myRedisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = myRedisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return myRedisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
*/
public long lGetListSize(String key) {
try {
return myRedisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return myRedisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
myRedisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
myRedisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
*/
public boolean lSet(String key, List<Object> value) {
try {
myRedisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
myRedisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
myRedisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = myRedisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
4.3 测试代码
@SpringBootTest
public class _MyRedis {
@Resource
private RedisTemplate<String, Object> myRedisTemplate;
@Autowired
private RedisUtil redisUtil;
@Test
void test01() {
User user = new User("林青霞", 30);
ValueOperations<String, Object> ops = myRedisTemplate.opsForValue();
ops.set("user", user);
System.out.println(ops.get("user")); // User(name=林青霞, age=30)
/*
Redis 查询结果:
127.0.0.1:6379> keys *
1) "user"
*/
}
@Test
void test02(){
redisUtil.set("name", "张三");
System.out.println(redisUtil.get("name"));
System.out.println(factory);
}
}
4.4 使用 Jedis 连接池
- 增加依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 配置参数
spring:
redis:
host: 127.0.0.1 # redis 的主机 ip
port: 6379
client-type: jedis
6、Mongodb
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
2. 配置参数
spring:
data:
mongodb:
uri: mongodb://localhost/erer
3. 测试代码
@SpringBootTest
class MongoDbTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
void save() {
Book book = new Book();
book.setId(1);
book.setName("springboot");
book.setType("springboot");
book.setDescription("springboot");
mongoTemplate.save(book);
}
@Test
void find() {
List<Book> all = mongoTemplate.findAll(Book.class);
for (Book book : all) {
System.out.println(book);
}
}
}
7、ElasticSearch
1. 引入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
2. 配置参数
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/rest
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: erer.elasticsearch.hotel.pojo
3. 测试代码
@SpringBootTest
public class _RestHighLevelClient {
private RestHighLevelClient client;
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.116.129:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
四、缓存解决方案
- 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质
- 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能
- 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间
1、Simple
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2. 自定义配置
package com.erer.gulimall.product.config;
// 开启缓存功能
@EnableCaching
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 自定义缓存的 k(String) v(json) 序列化方式
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 获取配置文件中所有的配置
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
3. 获取缓存
- 此类注入了spring容器,只有容器调用 get(),@Cacheable才会起作用。
package erer.springboot.cache.utils;
@Component
public class CodeUtils {
// 获取缓存
@Cacheable(value = "smsCode", key = "#tele")
public String get(String tele) {
return null;
}
// 生成验证码
public String generator(String tele) {
StringBuilder checkCode = new StringBuilder();
int hash = tele.hashCode();
long nowTime = System.currentTimeMillis();
Random ran = new Random();
long result = hash ^ nowTime ^ ran.nextInt(20220321);
for (int i = 0; i < 6; i++) {
long temp = result % 10;
temp = temp < 0 ? -temp : temp;
checkCode.append(temp);
result /= 10;
}
return checkCode.toString();
}
public String generator2(String tele) {
StringBuilder checkCode = new StringBuilder();
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 生成随机数
Random ran = new Random();
for (int i = 0; i < 6; i++) {
int index = ran.nextInt(str.length());
char ch = str.charAt(index);
checkCode.append(ch);
}
return checkCode.toString();
}
}
4. 存入缓存
package erer.springboot.cache.service.impl;
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Override
// 将验证码放入缓存
@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
return codeUtils.generator(tele);
}
@Override
public boolean checkCode(SMSCode smsCode) {
String code = smsCode.getCode();
String cacheCode = codeUtils.get(smsCode.getTele());
return cacheCode.equals(code);
}
}
5. Controller
package erer.springboot.cache.controller;
@RestController
@RequestMapping("/sms")
public class SMSCodeController {
@Autowired
private SMSCodeService service;
@RequestMapping("/tele")
public String sendCodeToSMS(String tele) {
return service.sendCodeToSMS(tele);
}
@RequestMapping("/check")
public boolean checkCode(String tele, String code) {
SMSCode smsCode = new SMSCode();
smsCode.setCode(code);
smsCode.setTele(tele);
System.out.println(smsCode);
return service.checkCode(smsCode);
}
}
2、Ehcache
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2. 配置参数
spring:
cache:
type: ehcache
ehcache:
config: ehcache.xml
3. 配置文件
- ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!-- 默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxElementsInMemory:最大缓存数量-->
<!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
<!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果-->
<!-- timeToLiveSeconds:最大存活时间-->
<!-- memoryStoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />
<!-- 配置 smsCode 缓存 -->
<cache
name="smsCode"
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
3、Redis
1. 引如依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置参数
# redis
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
# 是否使用前缀名(系统定义前缀名)
use-key-prefix: true
# 追加自定义前缀名
key-prefix: sms_
# 有效时长
time-to-live: 10s
# 是否允许存储空值(防止缓存穿透)
cache-null-values: true
3. 注解
-
@Cacheable(value = {“category”}, key = “leavl1Category”)
- 标注当前方法的结果需要缓存,如果已缓存直接获取;如果没有缓存,调用方法后将结构缓存
- value:缓存分区
- key:缓存名称
-
@CacheEvict(value = “category”, key = “leavl1Category”)
- 指定删除某个分区下的所有数据
- value:缓存分区
- key:缓存名称
- allEntries = true:删除当前分区的全部缓存
-
@Caching
- 多个操作
@Caching(evict = { @CacheEvict(value = "category", key = "leavl1Category"), @CacheEvict(value = "category", key = "getCatalogJson") })
- 多个操作
4、Memcached
1. 引入依赖
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.7</version>
</dependency>
2. 配置参数
memcached:
servers: localhost:11211
poolSize: 10
opTimeout: 3000
3. 加载配置
package erer.springboot.cache.config;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Component
@ConfigurationProperties(prefix = "memcached")
public class XMemcachedProperties {
// 链接地址
private String servers;
// 连接池数量
private Integer poolSize;
// 超时时间
private Long opTimeout;
}
4. 配置类
package erer.springboot.cache.config;
@Configuration
public class XMemcachedConfig {
@Autowired
private XMemcachedProperties xMemcachedProperties;
@Bean
public MemcachedClient getMemcachedClient() throws IOException {
MemcachedClientBuilder builder = new XMemcachedClientBuilder(xMemcachedProperties.getServers());
builder.setConnectionPoolSize(xMemcachedProperties.getPoolSize());
builder.setConnectTimeout(xMemcachedProperties.getOpTimeout());
return builder.build();
}
}
5. 存取缓存信息
package erer.springboot.cache.service.impl;
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private MemcachedClient memcachedClient;
@Override
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
try {
// key , 过期时间(0代表永不过期),value
memcachedClient.set(tele, 10, code);
} catch (TimeoutException | InterruptedException | MemcachedException e) {
e.printStackTrace();
}
return code;
}
@Override
public boolean checkCode(SMSCode smsCode) {
String code = null;
try {
code = memcachedClient.get(smsCode.getTele());
} catch (TimeoutException | InterruptedException | MemcachedException e) {
e.printStackTrace();
}
return smsCode.getCode().equals(code);
}
}
5、Jetcache
- jetCache对SpringCache进行了封装,在原有功能基础上实现了多级缓存、缓存统计、自动刷新、异步调用、数据报表等功能
- jetCache设定了本地缓存与远程缓存的多级缓存解决方案
- 本地缓存(local):LinkedHashMap、Caffeine
- 远程缓存(remote):Redis、Tair
1. 引入依赖
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>
2. 开启缓存
package erer.springboot.cache;
@SpringBootApplication
// 开启缓存功能
@EnableCreateCacheAnnotation
// 开启方法注解缓存
@EnableMethodCache(basePackages = "erer.springboot.cache.service")
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
3. 配置参数
jetcache:
# 统计间隔,0表示不统计
statIntervalMinutes: 15
areaInCacheName: false
local:
default:
type: linkedhashmap
keyConvertor: fastjson
limit: 100
ssm:
type: linkedhashmap
keyConvertor: fastjson
limit: 100
remote:
default:
type: redis
host: localhost
port: 6379
# 缓存对象序列化
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
ssm:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
spring:
main:
# 循环依赖
allow-circular-references: true
# 配置数据源 DataSource
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot
username: root
password: 123456
mybatis-plus:
# MyBatis 配置文件位置
# config-location: classpath:mybatis-config.xml
# MyBatis 配置映射文件路径:
mapper-locations: classpath:erer/springboot/mybatisplus/*Mapper.xml
# MyBaits 别名包扫描路径
type-aliases-package: erer.springboot.mybatisplus.domain
configuration:
# 是否开启自动驼峰命名规则(camel case)映射
# 即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。
# 此属性在 MyBatis 中原默认值为 false,MyBatisPlus 中默认值为 true
map-underscore-to-camel-case: false
# 开启日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局默认参数设置
global-config:
db-config:
# 主键类型:设置后,即可省略实体对象中的 @TableId(type = IdType.AUTO) 配置。
id-type: auto
# 表名前缀:设置后,即可省略实体对象中的 @TableName("tb_user") 配置
table-prefix: tb_
属性 | 默认值 | 说明 |
---|---|---|
jetcache.statIntervalMinutes | 0 | 统计间隔,0表示不统计 |
jetcache.hiddenPackages | 无 | 自动生成name时,隐藏指定的包名前缀 |
jetcache.[local|remote].${area}.type | 无 | 缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair |
jetcache.[local|remote].${area}.keyConvertor | 无 | key转换器,当前仅支持fastjson |
jetcache.[local|remote].${area}.valueEncoder | java | 仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.valueDecoder | java | 仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.limit | 100 | 仅local类型的缓存需要指定,缓存实例最大元素数 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis | 无穷大 | 默认过期时间,毫秒单位 |
jetcache.local.${area}.expireAfterAccessInMillis | 0 | 仅local类型的缓存有效,毫秒单位,最大不活动间隔 |
4. 存取缓存信息
package erer.springboot.cache.service.impl;
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
// name:缓存名称 expire:失效时间 timeUnit:时间单位 cacheType:远程/本地
@CreateCache(area = "ssm", name = "jetCache:", expire = 10, timeUnit = TimeUnit.SECONDS, cacheType = CacheType.BOTH)
private Cache<String, String> jetCache;
// jetcache
@Override
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele, code);
return code;
}
@Override
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
5. 缓存对象序列化
package erer.springboot.cache.domain;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("userInfo")
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private Integer gender;
private int age;
private String address;
private String qq;
private String email;
}
6. Mapper
package erer.springboot.cache.mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
7. Service
package erer.springboot.cache.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public boolean save(User user) {
return userMapper.insert(user) > 0;
}
@Override
// 删除缓存
@CacheInvalidate(name = "user_", key = "#id")
public boolean removeById(Integer id) {
return userMapper.deleteById(id) > 0;
}
@Override
// 更新缓存
@CacheUpdate(name = "user_", key = "#user.id", value = "#user")
public boolean updateById(User user) {
return userMapper.updateById(user) > 0;
}
@Override
// 添加缓存
@Cached(name = "user_", key = "#id", expire = 3600, cacheType = CacheType.BOTH)
// 自动刷新
// @CacheRefresh(refresh = 10, timeUnit = TimeUnit.SECONDS)
public User getById(Integer id) {
return userMapper.selectById(id);
}
@Override
public List<User> list() {
return userMapper.selectList(null);
}
}
8. Controller
package erer.springboot.cache.controller;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public boolean save(@RequestBody User user) {
return userService.save(user);
}
@PutMapping
public boolean update(@RequestBody User user) {
return userService.updateById(user);
}
@DeleteMapping("/{id}")
private boolean delete(@PathVariable Integer id) {
return userService.removeById(id);
}
@GetMapping("/{id}")
public User getById(@PathVariable Integer id) {
return userService.getById(id);
}
@GetMapping
public List<User> getAll() {
return userService.list();
}
}
6、J2cache
- j2cache是一个缓存整合框架,可以提供缓存的整合方案,使各种缓存搭配使用,自身不提供缓存功
- 基于 ehcache + redis 进行整合
1. 引入依赖
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-spring-boot2-starter</artifactId>
<version>2.8.0-release</version>
</dependency>
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-core</artifactId>
<version>2.8.4-release</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2. 配置参数
j2cache:
config-location: j2cache.properties
3. 配置文件
- j2cache.properties
# 配置1级缓存
j2cache.L1.provider_class = ehcache
# 配置1级缓存的配置文件
ehcache.configXml = ehcache.xml
# 配置2级缓存
j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379
# 配置1级缓存数据到2级缓存的广播方式:可以使用redis提供的消息订阅模式,也可以使用jgroups多播实现
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
- ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!-- 默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxElementsInMemory:最大缓存数量-->
<!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
<!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果-->
<!-- timeToLiveSeconds:最大存活时间-->
<!-- memoryStoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
4. 存取缓存信息
package erer.springboot.cache.service.impl;
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private CacheChannel cacheChannel;
@Override
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
cacheChannel.set("sms", tele, code);
return code;
}
@Override
public boolean checkCode(SMSCode smsCode) {
String code = cacheChannel.get("sms", smsCode.getTele()).asString();
return smsCode.getCode().equals(code);
}
}
7、SpringSession
1. 引入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2. 配置参数
spring:
# 配置spring-session
session:
store-type: redis
3. 配置文件
package com.erer.gulimall.auth.config;
@Configuration
// 开启 redis 作为 session 的存储
@EnableRedisHttpSession
public class SpringSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 放大 session 的作用域:默认发的令牌,仅作用于当前域
cookieSerializer.setDomainName("gulimall.com");
// 自定义 令牌名称
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// 使用JSON的序列化方式来序列化对象到Redis中
return new GenericJackson2JsonRedisSerializer();
}
}
4. 存入信息
package com.erer.gulimall.auth.controller;
@Controller
public class OAuth2Controller {
@Resource
private MemberFeignService memberFeignService;
@GetMapping(value = "/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 第一次使用 session 命令浏览器保存卡号,JSESSIONID这个cookie,
// 以后浏览器访问哪个网站就会带上这个网站的cookie
// String LOGIN_USER = "loginUser"
session.setAttribute(AuthServerConstant.LOGIN_USER, userId);
}
}
5. 获取信息
package com.erer.gulimall.order.interceptor;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
// 把登录后用户的信息放在ThreadLocal里面进行保存
MemberHolder.saveMember(attribute);
return true;
} else {
// 未登录,返回登录页面
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
五、整合其他框架
1、Junit
1. 搭建工程
package erer.springboot.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. 引入依赖
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. 编写测试类
package erer.springboot.springboot.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void add(){
System.out.println("add...");
}
}
4. 添加注解
- @SpringBootTest(classes = 启动类.class)
4.1 同包下
package erer.springboot.springboot;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
void test(){
userService.add();
}
}
4.2 不同包下
package erer;
@SpringBootTest(classes = Application.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
void test(){
userService.add();
}
}
5.加载测试专用属性
5.1 通过配置文件加载
test:
prop: testValue
- 测试代码
@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Value("${test.prop}")
private String msg;
@Test
void contextLoads() {
System.out.println(msg); // testValue
}
}
5.2 properties 临时属性
@SpringBootTest(properties = {"test.prop=testValue1"})
class ApplicationTests {
@Value("${test.prop}")
private String msg;
@Test
void contextLoads() {
System.out.println(msg); // testValue1
}
}
5.3 args 临时参数
@SpringBootTest(args={"--test.prop=testValue2"})
class ApplicationTests {
@Value("${test.prop}")
private String msg;
@Test
void contextLoads() {
System.out.println(msg); // testValue2
}
}
6.Web环境模拟测试
-
启动web环境:webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
- MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
- DEFINED_PORT:使用自定义的端口作为web服务器端口
- RANDOM_PORT:使用随机端口作为web服务器端口
- NONE:不启动web环境
-
开启虚拟MVC调用:@AutoConfigureMockMvc
-
初始化虚拟调用对象:MockMVC
-
创建虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求
// 1、开启web虚拟调用功能
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 2、开启虚拟MVC调用
@AutoConfigureMockMvc
class WebTests {
@Test
// 3、初始化虚拟调用对象:MockMVC
void contextLoads(@Autowired MockMvc mvc) throws Exception {
// 4、创建虚拟请求,当前访问 http://localhost:8080//employees/1002
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/employees/1002");
// 5、执行对应的请求
ResultActions action = mvc.perform(builder);
// 6、响应状态匹配
// 6.1 定义本次调用的预期值
StatusResultMatchers status = MockMvcResultMatchers.status();
// 6.2 预计本次调用时成功的:状态200
ResultMatcher ok = status.isOk();
// 6.3 添加预计值到本次调用过程中进行匹配
action.andExpect(ok);
// 7、响应头信息匹配
// 7.1 定义本次调用的预期值
HeaderResultMatchers header = MockMvcResultMatchers.header();
// 7.2 预计本次调用时成功信息
ResultMatcher contentType = header.string("Content-Type", "application/json");
// 7.3 添加预计值到本次调用过程中进行匹配
action.andExpect(contentType);
// 8、 响应体匹配(非json数据格式)
// 8.1 定义本次调用的预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
// 8.2 预计本次调用时成功信息
ResultMatcher result = content.string("springboot2");
// 8.3 添加预计值到本次调用过程中进行匹配
action.andExpect(result);
// 9、响应体匹配(json数据格式,开发中的主流使用方式)
// 9.1 定义本次调用的预期值
// ContentResultMatchers content = MockMvcResultMatchers.content();
// 9.2 预计本次调用时成功信息
result = content.json("{\"flag\":true,\"data\":{\"id\":1002,\"lastName\":\"lisi\",\"email\":\"978786080@qq.com\",\"gender\":0,\"birthday\":\"2021-09-01T16:00:00.000+00:00\",\"department\":102},\"msg\":null}");
// 9.3 添加预计值到本次调用过程中进行匹配
action.andExpect(result);
}
}
7. 数据层测试回滚
- 为测试用例添加事务:@Transactional 事务不会提交
@SpringBootTest
@Transactional
class DaoTests {
@Autowired
private IEmployeeService employeeService;
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setGender(1);
employee.setEmail("978786080@qq.com");
employee.setLastName("erer");
employeeService.save(employee);
}
}
- 在测试用例中提交事务,可以通过@Rollback注解设置
@SpringBootTest
@Transactional
@Rollback(value = false)
class DaoTests {
@Autowired
private IEmployeeService employeeService;
@Test
void contextLoads() {
Employee employee = new Employee();
employee.setGender(1);
employee.setEmail("978786080@qq.com");
employee.setLastName("erer");
employeeService.save(employee);
}
}
8.测试用例数据设定
-
application.yml
- ${random.int}表示随机整数
- ${random.int(10)}表示10以内的随机数
- ${random.int(10,20)}表示10到20的随机数
- 其中()可以是任意字符,例如[],! !均可
testcase:
book:
id: ${random.int}
id2: ${random.int(10)}
type: ${random.int!5,10!}
name: ${random.value}
uuid: ${random.uuid}
publishTime: ${random.long}
- domin
package erer.springboot.ssmp.domain;
@Component
@Data
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {
private int id;
private int id2;
private int type;
private String name;
private String uuid;
private long publishTime;
}
- 测试(与 domin 在同一包下)
package erer.springboot.ssmp;
@SpringBootTest
class DaoTests {
@Autowired
private BookCase bookCase;
@Test
void test() {
System.out.println(bookCase);
}
}
2、Druid
1. 引入依赖
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
2. 配置参数
spring:
# 使用 druid 数据源
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///springboot
username: root
password: 123456
# Druid 数据源专有配置,Spring Boot默认不注入这些配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
poolPreparedStatements: true
# 配置监控统计拦截的filters,stat:监控统计、log4j:日志纪录、wall:防御sql注入
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
3. 配置后台监控功能
package erer.springboot.mybatis.config;
/*
自定义 Druid 的 bean
*/
@Configuration // 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解
public class DruidConfig {
@Bean // 使用在方法上,标注将该方法的返回值存储到 Spring 容器中
@ConfigurationProperties(prefix = "spring.datasource") // 绑定配置文件相关配置
@ConditionalOnMissingBean(name = "druidDataSource") // 如果 spring 的 IOC 容器中 不存在 jedis 的 bean,才创建
public DataSource druidDataSource() {
return new DruidDataSource();
}
// 配置后台监控功能:类似于 web.xml(因为 springboot 内置了 servlet 容器,所有没有 web.xml)
@Bean // 使用在方法上,标注将该方法的返回值存储到 Spring 容器中
public ServletRegistrationBean<StatViewServlet> servletRegistrationBean() {
ServletRegistrationBean<StatViewServlet> statViewServletBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 配置登录账号、密码
HashMap<String, String> initParameters = new HashMap<>();
initParameters.put("loginUsername", "admin");
initParameters.put("loginPassword", "123456");
// 允许访问
initParameters.put("allow", ""); // "":所有人、"localhost":允许本机访问
// 禁止访问
// initParameters.put("erer", "192.168.11.126");
// 设置初始化参数
statViewServletBean.setInitParameters(initParameters);a
return statViewServletBean;
}
// 配置过滤器
@Bean // 使用在方法上,标注将该方法的返回值存储到 Spring 容器中
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filer = new FilterRegistrationBean<>();
// 过滤器
filer.setFilter(new WebStatFilter());
// 设置需要过滤的请求
HashMap<String, String> initParameters = new HashMap<>();
// 以下文件不进行统计
initParameters.put("exclusions", "*.js, *.css, /druid/**");
// 设置初始化参数
filer.setInitParameters(initParameters);
return filer;
}
}
4. 查看监控
查看地址:http://localhost:8080/druid
3、MyBatis
1. 引入依赖
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
2. 配置参数
spring:
# 使用 druid 数据源
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///springboot
username: root
password: 123456
3. 定义数据表
4. 定义实体类
package erer.springboot.mybatis.domain;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String username;
private String password;
}
5. 注解开发
5.1 mapper文件
package erer.springboot.mybatis.mapper;
@Mapper // 表示这是一个 mybatis 的 mapper 类
public interface UserMapper {
@Select("select * from t_user")
List<User> findAll();
}
5.2 测试代码
@SpringBootTest
class Mybatis {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
System.out.println(userMapper.findAll());
}
}
6. XML开发
6.1 mapper文件
package erer.springboot.mybatis.mapper;
@Mapper // 表示这是一个 mybatis 的 mapper 类
public interface UserXmlMapper {
List<User> findAll();
}
6.2 映射文件
<?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">
<mapper namespace="erer.springboot.mybatis.mapper.UserXmlMapper">
<select id="findAll" resultType="user">
select * from t_user
</select>
</mapper>
6.3 配置参数
# 配置 mybatis
mybatis:
# configuration: 配置 核心文件
# 映射文件路径(Mapper.xml 路径 与 UserXmlMapper 类包名相同时无需配置)
mapper-locations: classpath:erer/mapper/*Mapper.xml
# 定义别名
type-aliases-package: erer.springboot.mybatis.domain
6.4 测试代码
@SpringBootTest
class Mybatis {
@Autowired
private UserXmlMapper userXmlMapper;
@Test
void test() {
System.out.println(userXmlMapper.findAll());
}
}
4、Mybatis-Plus
1. 引入依赖
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
2. 配置参数
spring:
# 配置数据源 DataSource
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot
username: root
password: 123456
mybatis-plus:
# MyBatis 配置文件位置
# config-location: classpath:mybatis-config.xml
# MyBatis 配置映射文件路径:
mapper-locations: classpath*:erer/springboot/mybatisplus/*Mapper.xml
# MyBaits 别名包扫描路径
type-aliases-package: erer.springboot.mybatisplus.domain
configuration:
# 是否开启自动驼峰命名规则(camel case)映射
# 即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。
# 此属性在 MyBatis 中原默认值为 false,MyBatisPlus 中默认值为 true
map-underscore-to-camel-case: false
# 开启日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局默认参数设置
global-config:
db-config:
# 主键自增:设置后,即可省略实体对象中的 @TableId(type = IdType.AUTO) 配置。
id-type: auto
# 表名前缀:设置后,即可省略实体对象中的 @TableName("tb_user") 配置
table-prefix: tb_
3. 定义实体类
package erer.springboot.mybatisplus.domain;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("userInfo")
public class User {
@TableId(type = IdType.AUTO)
private int id;
private String name;
private Integer gender;
private int age;
private String address;
private String qq;
private String email;
}
4. Mapper
package erer.springboot.mybatisplus.mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
5. Service接口
package erer.springboot.mybatisplus.service;
public interface IUserService extends IService<User> {
// 分页展示
IPage<Employee> getPage(Integer currentPage, Integer pageSize);
// 查询分页
IPage<User> getPage(Integer currentPage, Integer pageSize, User user);
}
6. Service实现类
package erer.springboot.mybatisplus.service.impl;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public IPage<User> getPage(Integer currentPage, Integer pageSize) {
IPage<User> page = new Page<>(currentPage, pageSize);
return userMapper.selectPage(page, null);
}
@Override
public IPage<User> getPage(Integer currentPage, Integer pageSize,User user) {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.like(Strings.isNotEmpty(user.getName()), User::getName, user.getName());
lqw.like(Strings.isNotEmpty(user.getEmail()), User::getEmail, user.getEmail());
lqw.like(user.getGender() != null, User::getGender, user.getGender());
IPage<User> page = new Page<>(currentPage, pageSize);
userMapper.selectPage(page, lqw);
return page;
}
}
7. 设置核心配置类
package erer.springboot.mybatisplus.config;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
// 注入分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1、定义 Mp 拦截器
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 2、添加具体的拦截器
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
// 自定义视图控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("pages/userList.html");
registry.addViewController("/index.html").setViewName("pages/userList.html");
}
}
8. 拦截异常信息
package erer.springboot.mybatisplus.controller.utils;
@RestControllerAdvice
public class ProjectExceptionAdvice {
// 拦截所以的异常信息
@ExceptionHandler
public E doException(Exception exception) {
// 记录日志
// 通知运维
// 通知开发
exception.printStackTrace();
return new E("服务器故障,请稍后再试!");
}
}
9. 与前端交互信息
package erer.springboot.mybatisplus.controller.utils;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class E {
private Boolean flag;
private Object data;
private String msg;
public E(Boolean flag) {
this.flag = flag;
}
public E(Boolean flag, Object data) {
this.flag = flag;
this.data =data;
}
public E(String msg) {
this.flag = false;
this.msg = msg;
}
}
10. Controller
package erer.springboot.ssmp.controller;
RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private IUserService userService;
// 添加用户
@PostMapping
public E save(@RequestBody User user) {
return new E(userService.save(user));
}
// 根据id更新用户
@PutMapping
public E update(@RequestBody User user) {
return new E(userService.updateById(user));
}
// 根据id删除用户
@DeleteMapping("/{id}")
private E delete(@PathVariable Integer id) {
return new E(userService.removeById(id));
}
// 根据id查询用户
@GetMapping("/{id}")
public E getById(@PathVariable Integer id) {
return new E(true, userService.getById(id));
}
// 获取所有用户信息
@GetMapping
public E getAll() {
return new E(true, userService.list());
}
// 分页查询
@GetMapping("/{currentPage}/{pageSize}")
public E getPage(@PathVariable Integer currentPage, @PathVariable Integer pageSize, User uder) {
IPage<User> page = userService.getPage(currentPage, pageSize, user);
// 当前页大于总页码,重新获取。
if(currentPage > page.getPages()) {
page = userService.getPage((int) page.getPages(), pageSize, user);
}
return new E(true, page);
}
}
11. 前端访问
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>基于SpringBoot整合SSM案例</title>
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- 引入样式 -->
<link rel="stylesheet" href="../plugins/elementui/index.css">
<link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/style.css">
</head>
<body class="hold-transition">
<div id="app">
<div class="content-header">
<h1>用户管理</h1>
</div>
<div class="app-container">
<div class="box">
<div class="filter-container">
<el-input placeholder="用户姓名" v-model="pagination.nName" style="width: 200px;"
class="filter-item"></el-input>
<el-input placeholder="电子邮箱" v-model="pagination.email" style="width: 200px;"
class="filter-item"></el-input>
<el-input placeholder="性别" v-model="pagination.gender" style="width: 200px;"
class="filter-item"></el-input>
<el-button @click="getAll()" class="dalfBut">查询</el-button>
<el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
</div>
<el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
<el-table-column type="index" label="序号" align="center"></el-table-column>
<el-table-column prop="nName" label="用户姓名" align="center"></el-table-column>
<el-table-column prop="email" label="电子邮箱" align="center"></el-table-column>
<el-table-column prop="gender" label="性别" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<div class="pagination-container">
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
<!-- 新增标签弹层 -->
<div class="add-form">
<el-dialog title="新增用户" :visible.sync="dialogFormVisible">
<el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
<el-row>
<el-col :span="22">
<el-form-item label="用户姓名" prop="name">
<el-input v-model="formData.name"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="22">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="formData.email"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="22">
<el-form-item label="性别">
<el-input v-model="formData.gender"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel()">取消</el-button>
<el-button type="primary" @click="handleAdd()">确定</el-button>
</div>
</el-dialog>
</div>
<!-- 编辑标签弹层 -->
<div class="add-form">
<el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">
<el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
<el-row>
<el-col :span="22">
<el-form-item label="用户姓名" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="22">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="formData.email"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="22">
<el-form-item label="性别">
<el-input v-model="formData.gender"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel()">取消</el-button>
<el-button type="primary" @click="handleEdit()">确定</el-button>
</div>
</el-dialog>
</div>
</div>
</div>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script>
var vue = new Vue({
el: '#app',
data: {
dataList: [],//当前页要展示的列表数据
dialogFormVisible: false,//添加表单是否可见
dialogFormVisible4Edit: false,//编辑表单是否可见
formData: {},//表单数据
rules: {//校验规则
name: [{required: true, message: '用户姓名为必填项', trigger: 'blur'}],
email: [{required: true, message: '电子邮箱为必填项', trigger: 'blur'}],
gender: [{required: true, message: '性别为必填项', trigger: 'blur'}]
},
pagination: {//分页相关模型数据
currentPage: 1,//当前页码
pageSize: 5,//每页显示的记录数
total: 0,//总记录数
name: "",
email: "",
gender: ""
}
},
//钩子函数,VUE对象初始化完成后自动执行
created() {
//调用查询全部数据的操作
this.getAll();
},
methods: {
//分页查询
getAll() {
//组织参数,拼接url请求地址
let param = "?name=" + this.pagination.name;
param += "&email=" + this.pagination.email;
param += "&gender=" + this.pagination.gender;
//发送异步请求
axios.get("/users/" + this.pagination.currentPage + "/" + this.pagination.pageSize + param).then((res) => {
this.pagination.pageSize = res.data.data.size;
this.pagination.currentPage = res.data.data.current;
this.pagination.total = res.data.data.total;
this.dataList = res.data.data.records;
});
},
//切换页码
handleCurrentChange(currentPage) {
//修改页码值为当前选中的页码值
this.pagination.currentPage = currentPage;
//执行查询
this.getAll();
},
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
// 重置表单
this.resetForm();
},
//重置表单
resetForm() {
this.formData = {};
},
//添加
handleAdd() {
axios({
method: 'post',
url: '/users',
data: JSON.stringify(this.formData),
headers: {'Content-Type': 'application/json;charset=UTF-8'}
}).then((res) => {
//判断当前操作是否成功
if (res.data.flag) {
//1.关闭弹层
this.dialogFormVisible = false;
this.$message.success("添加成功");
} else {
this.$message.error("添加失败");
}
}).finally(() => {
//2.重新加载数据
this.getAll();
});
},
//取消
cancel() {
this.dialogFormVisible = false;
this.dialogFormVisible4Edit = false;
this.$message.info("当前操作取消");
},
// 删除
handleDelete(row) {
console.log(row);
this.$confirm("此操作永久删除当前信息,是否继续?", "提示", {type: "info"}).then(() => {
axios.delete("/users/" + row.id).then((res) => {
if (res.data.flag) {
this.$message.success("删除成功");
} else {
this.$message.error("数据同步失败,自动刷新");
}
}).finally(() => {
//2.重新加载数据
this.getAll();
});
}).catch(() => {
this.$message.info("取消操作");
});
},
//弹出编辑窗口
handleUpdate(row) {
axios.get("/users/" + row.id).then((res) => {
if (res.data.flag && res.data.data != null) {
this.dialogFormVisible4Edit = true;
this.formData = res.data.data;
} else {
this.$message.error("数据同步失败,自动刷新");
}
}).finally(() => {
//2.重新加载数据
this.getAll();
});
},
//修改
handleEdit() {
axios({
method: 'put',
url: '/users',
data: JSON.stringify(this.formData),
headers: {'Content-Type': 'application/json;charset=UTF-8'}
}).then((res) => {
//判断当前操作是否成功
if (res.data.flag) {
//1.关闭弹层
this.dialogFormVisible4Edit = false;
this.$message.success("修改成功");
} else {
this.$message.error("修改失败");
}
}).finally(() => {
//2.重新加载数据
this.getAll();
});
},
}
})
</script>
</html>
5、SpringSecurity
1. 引入依赖
<!-- thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- web 启动依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security 启动依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- security-thymeleaf 整合依赖 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
2. 页面跳转
package erer.springboot.security.controller;
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
System.out.println(1);
return "index";
}
@RequestMapping({"/login"})
public String login() {
return "views/login";
}
@RequestMapping({"/level1/{id}"})
public String level1(@PathVariable("id") int id) {
return "views/level1/" + id;
}
@RequestMapping({"/level2/{id}"})
public String level2(@PathVariable("id") int id) {
return "views/level2/" + id;
}
@RequestMapping({"/level3/{id}"})
public String level3(@PathVariable("id") int id) {
return "views/level3/" + id;
}
}
3. SpringSecurity
package erer.springboot.security.config;
@EnableWebSecurity
public class securityConfig extends WebSecurityConfigurerAdapter {
// 授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,功能页只有对应权限的人才能访问
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasAnyRole("vip1")
.antMatchers("/level2/**").hasAnyRole("vip2")
.antMatchers("/level3/**").hasAnyRole("vip3");
// 没有权限会跳转到登录页面
http.formLogin()
.loginPage("/login") // 定制登录页
// .loginProcessingUrl("/login")
.usernameParameter("name") // 工号 默认 name 为 username,这里可以进行修改
.passwordParameter("pwd"); // 密码 默认 name 为 password,这里可以进行修改
// 开启注销功能
http.logout()
.deleteCookies("remove") // 移除所有的 cookie * 慎重使用
.invalidateHttpSession(false) // 清除所有的 session * 慎重使用
.logoutSuccessUrl("/") // 注销成功后前往首页
.and()
.csrf().disable(); // 注销失败可能存在的原因,关闭 csrf 功能
// 开启记住我的功能:默认保存 2 周
http.rememberMe()
.rememberMeParameter("remember"); // 登录页面 记住我 的 name 一致
}
// 认证
// spring security 5.X版本需要提供一个 passwordEncoder 的实例,否则后台汇报错误:There is no PasswordEncoder mapped for the id "null"
// 因此,需要创建 passwordEncoder 的实现类,并使用 @Component 注解将其声明为 spring 组件即可生效
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 正常从数据库读取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and()
.withUser("erer").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3");
}
}
4. 静态页面
<!DOCTYPE html>
<!-- 导入命名空间 -->
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>首页</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
<link th:href="@{/css/qinstyle.css}" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--登录注销-->
<div class="right menu">
<!--如果没有登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/login}">
<i class="address card icon"></i> 登录
</a>
</div>
<!-- 如果登录:用户信息 -->
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<!-- 如果登录:注销 -->
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
</div>
</div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study</h3>
</div>
<div>
<br>
<div class="ui three column stackable grid">
<!--动态菜单的效果-->
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/js/semantic.min.js}"></script>
</body>
</html>
6、Shiro
三大对象:
- Subject:用户
- SecurityManager:管理所以用户
- Realm:连接数据
1. 引入依赖
<!-- shiro 启动依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.8.0</version>
</dependency>
<!-- thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- shiro-thymeleaf 整合依赖 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
2. 创建realm对象
package erer.springboot.shiro.config;
/*
自定义的 UserRealm
1、继承 AuthorizingRealm
2、doGetAuthenticationInfo(认证)方法
• RouterController 类中,登录验证方法 获取当前的用户、封装数据、执行登录操作
• doGetAuthenticationInfo 方法中 连接数据库验证用户
3、实现 doGetAuthorizationInfo(授权)方法
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
// 实现 授权 方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 获取当前登录对象
Subject subject = SecurityUtils.getSubject();
User user =(User) subject.getPrincipal();
// 设置当前用户的权限
info.addStringPermission(user.getVipLevel());
return info;
}
// 实现 认证 方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取登录信息
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
// 连接数据库,查找用户
User user = userMapper.queryByName(userToken.getUsername());
if (user == null) {
return null; // 抛出 UnknownAccountException 异常
}
// 密码认证 shiro 自动完成
// 可以加密:MD5、MD5盐值加密
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
3. 创建ShiroConfig
-
注入UserRealm
-
创建 DefaultWebSecurityManager 方法
-
创建 ShiroFilterFactoryBean 方法(添加内置过滤器)
- anon:无需 登录 即可访问
- authc:必须 登录 才可访问
- user:必须用有 记住我功能才能使用
- perms:用有对 某个资源 的权限才可以访问,对于多个 资源 的权限,需要重写方法
- role:用有 某个角色 的权限才可以访问
package erer.springboot.shiro.config;
@Configuration
public class ShiroConfig {
// 创建 realm 对象,注入spring 容器
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 创建 DefaultWebSecurityManager
@Bean(name = "dSecurityManager")
// @Qualifier:绑定 spring 容器中的 realm 对象
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 关联 userRealm
defaultWebSecurityManager.setRealm(userRealm);
return defaultWebSecurityManager;
}
@Bean
// @Qualifier:绑定 spring 容器中的 defaultWebSecurityManager 对象
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("dSecurityManager")DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 添加安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 自定义过滤器集合
Map<String, Filter> myFilterMap = new HashMap<>();
// 创建自定义过滤器
RoleOrFilter roleOrFilter = new RoleOrFilter();
// 将自定义过滤器添加到集合
myFilterMap.put("s-perms",roleOrFilter); //可以配置RoleOrFilter的Bean
// 添加自定义过滤器
shiroFilterFactoryBean.setFilters(myFilterMap);
// 内置过滤器集合
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 登录拦截
filterChainDefinitionMap.put("/", "anon"); // anon:无需 登录 即可访问
filterChainDefinitionMap.put("/level1/*", "authc"); // authc:必须 登录 才可访问
filterChainDefinitionMap.put("/level2/*", "s-perms[vip2 | vip3 ]"); // 等级高于 vip2 才可以访问
filterChainDefinitionMap.put("/level3/*", "perms[vip3]"); // vip3 才可以访问
// 设置注销
filterChainDefinitionMap.put("/logout","logout");
// 添加内置过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 添加登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 添加未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
// Shiro 整合 thymeleaf
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
- 自定义拦截器:拓展 “|” 符号
package erer.springboot.shiro.config;
@Component
public class RoleOrFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = this.getSubject(request, response);
String[] perms = (String[]) (mappedValue);
boolean isPermitted = true;
if (perms != null && perms.length > 0) {
if (perms.length == 1) {
isPermitted = isOneOfPermitted(perms[0], subject);
} else {
isPermitted = isAllPermitted(perms,subject);
}
}
return isPermitted;
}
/**
* 以 "," 分割的权限为并列关系的权限控制,分别对每个权限字符串进行 "|" 分割解析
* 若并列关系的权限有一个不满足则返回false
* @param permStrArray 以 "," 分割的权限集合
* @param subject 当前用户的登录信息
* @return 是否拥有该权限
*/
private boolean isAllPermitted(String[] permStrArray, Subject subject) {
for (String s : permStrArray) {
if (!isOneOfPermitted(s, subject)) {
return false;
}
}
return true;
}
/**
* 判断以 "|" 分割的权限有一个满足的就返回 true,表示权限的或者关系
* @param permStr 权限数组种中的一个字符串
* @param subject 当前用户信息
* @return 是否有权限
*/
private boolean isOneOfPermitted(String permStr, Subject subject) {
String[] permArr = permStr.split("\\|");
if (permArr.length > 0) {
for (String perm : permArr) {
if (subject.isPermitted(perm)) {
return true;
}
}
}
return false;
}
}
4. mapper
package erer.springboot.shiro.mapper;
import erer.springboot.shiro.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Repository // 使用在 dao 层类上用于实例化 Bean
@Mapper // 表示这是一个 mybatis 的 mapper 类
public interface UserMapper {
@Select("select * from user where username = #{name}")
User queryByName(String name);
}
5. 页面跳转
package erer.springboot.shiro.controller;
@SuppressWarnings("SpringMVCViewInspection")
@Controller
public class RouterController {
@RequestMapping("/verification")
public String verification(String username, String password, Model model) {
// 获取当前的用户
Subject subject = SecurityUtils.getSubject();
// 封装用户的登录数据(类似 session 域,全局可以获得)
UsernamePasswordToken userToken = new UsernamePasswordToken(username, password);
// 执行登录操作
try {
subject.login(userToken);
return "index";
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户名不存在");
return "views/login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "views/login";
}
}
// 未授权页面跳转
@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
return "未授权,无法访问!";
}
// 注销登录
@RequestMapping("/logout")
public String logout(){
return "redirect:/index";
}
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping({"/login"})
public String login() {
return "views/login";
}
@RequestMapping({"/level1/{id}"})
public String level1(@PathVariable("id") int id) {
return "views/level1/" + id;
}
@RequestMapping({"/level2/{id}"})
public String level2(@PathVariable("id") int id) {
return "views/level2/" + id;
}
@RequestMapping({"/level3/{id}"})
public String level3(@PathVariable("id") int id) {
return "views/level3/" + id;
}
}
6. Shiro整合thymeleaf
操作 | 代码 |
---|---|
判读是否是游客,如果是未认证,也未记住的用户,那么就显示该标签中的内容 | shiro:guest=“” |
认证通过或已记住的用户,则显示该标签中的内容 | shiro:user=“” |
验证当前用户是否具有该 admin 角色,若拥有,则显示标签的内容 | shiro:principal |
输出当前用户信息,通常为登录帐号信息 | shiro:hasRole=“admin” |
与 hasRole 标签逻辑相反,当用户不属于该 developer 角色时显示 | shiro:lacksRole=“developer” |
验证当前用户是否同时具有以下所有角色 | shiro:hasAllRoles=“developer, product” |
验证当前用户是否具于以下任意一个角色 | shiro:hasAnyRoles=“admin, vip, developer” |
验证当前用户是否拥有以下权限 | shiro:hasPermission=“vip2” |
验证当前用户是否拥有以下所有角色 | shiro:hasAllPermissions=“vip2, vip3” |
验证当前用户是否拥有以下任意一个权限 | shiro:hasAnyPermissions=“vip2, vip3” |
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/extras/spring-shiro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>首页</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
<link th:href="@{/css/qinstyle.css}" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--登录注销-->
<div class="right menu">
<!-- 判读是否是游客,如果是未认证,也未记住的用户,那么就显示该标签中的内容-->
<div shiro:guest="">
<a class="item" th:href="@{/login}">
<i class="address card icon"></i> 登录
</a>
</div>
<!-- 认证通过或已记住的用户,则显示该标签中的内容 -->
<div shiro:user="">
<a class="item">
用户名:<span shiro:principal property="username"> </span>
角色:<span shiro:principal property="vipLevel"> </span>
</a>
</div>
<!-- 认证通过或已记住的用户,则显示该标签中的内容 -->
<div shiro:user="">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
</div>
</div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study</h3>
</div>
<div>
<br>
<div class="ui three column stackable grid">
<!--动态菜单的效果-->
<div class="column">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" shiro:hasAnyPermissions="vip2,vip3">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" shiro:hasPermission="vip3">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/js/semantic.min.js}"></script>
</body>
</html>
7、定时任务
- 启动项目自动执行
1.Quartz
- 工作(Job):用于定义具体执行的工作
- 工作明细(JobDetail):用于描述定时工作相关的信息
- 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则
- 调度器(Scheduler):描述了工作明细与触发器的对应关系
1.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
1.2 创建任务
package erer.sptingboot.task.quartz;
public class MyQuartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("quartz job run... ");
}
}
1.3 绑定触发器
package erer.sptingboot.task.config;
@Configuration
public class QuartzConfig {
@Bean
public JobDetail printJobDetail(){
// 绑定具体的工作 storeDurably()持久化
return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
}
@Bean
// 触发器
public Trigger printJobTrigger() {
// 绑定对应工作触发条件(每3秒执行一次)
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/3 * * * * ?");
return TriggerBuilder.newTrigger().forJob(printJobDetail())
.withSchedule(cronScheduleBuilder).build();
}
}
2. Task
- TaskScheduler:任务调度者
- TaskExecutor:任务执行者
2.1开启定时任务
package erer.sptingboot.task;
@SpringBootApplication
// 自动开启定时任务功能
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.2 创建定时任务
package erer.sptingboot.task.quartz;
// 注入容器
@Component
public class ScheduledService {
// 定时任务
//@Scheduled(cron = "0 34 6 4 10 0-7") // cron = "秒 分 时 日 月 星期"
//@Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
@Scheduled(cron = "*/5 * * * * ?") // 每5秒执行一次
public void hello () {
System.out.println("hello world!");
}
}
2.3 配置参数
spring:
task:
scheduling:
# 任务调度线程池大小 默认 1
pool:
size: 1
# 调度线程名称前缀 默认 scheduling-
thread-name-prefix: ssm_
shutdown:
# 线程池关闭时等待所有任务完成
await-termination: false
# 调度线程关闭前最大等待时间,确保最后一定关闭
await-termination-period: 10s
8、JavaMail
- SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
- POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
- IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议
1. 引入依赖
<!-- mail 启动依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2. 配置文件
spring:
# 设置邮箱参数
mail:
host: smtp.qq.com
username: 978786080
password: kkqqujtuwmgmbdjg
# QQ邮箱 特有开启加密验证
properties:
mail.smtp.ssl.enable: true
3. 导入 JavaMailSenderImpl
package erer.sptingboot.task;
@SpringBootTest
class _邮件任务 {
@Autowired
private JavaMailSenderImpl mailSender;
@Test
void test01() {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("尔尔,你好!");
mailMessage.setText("最难不过坚持!");
mailMessage.setTo("978786080@qq.com", "313082141@qq.com");
mailMessage.setCc("313082141@qq.com");
mailMessage.setFrom("978786080@qq.com" + "(尔尔)");
mailSender.send(mailMessage);
}
@Test
void test02() throws MessagingException {
// 一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 组装(true:代表添加附件)
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
// 标题
helper.setSubject("尔尔,你好!");
// 正文(true:代表开启html验证)
helper.setText("<p style='color:red'>最难不过坚持!</p>", true);
// 附件(图片名字带后缀有预览效果)
helper.addAttachment("1.jpg",new File("src/main/resources/static/02.jpg"));
// 收\发件人信息
helper.setTo("978786080@qq.com");
helper.setCc("313082141@qq.com");
helper.setFrom("978786080@qq.com");
// 发送
mailSender.send(mimeMessage);
}
}
9、异步任务
1. 开启异步任务
package erer.sptingboot.task;
@SpringBootApplication
// 自动开启异步任务功能
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. 创建异步任务
package erer.sptingboot.task.service;
@Service
public class AsyncService {
@Async // 告诉 spring 这是一个异步的方法
public void hello() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理!");
}
}
3. 测试方法
package erer.sptingboot.task.controller;
@RestController
public class AsyncController {
@Autowired
private AsyncService asyncService;
@RequestMapping("/hello")
public String hello() {
asyncService.hello();
return "ok";
}
}
10、Swagger
1. 引入依赖
<!-- swagger 依赖 -->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>
2. 创建SwaggerConfig
package erer.springboot.swagger.config;
@Configuration // 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解
@EnableSwagger2 // 开启 Swagger2
public class SwaggerConfig {
}
3. 访问测试
- 访问 http://localhost:8080/swagger-ui.html
4.配置Swagger
package erer.springboot.swagger.config;
@Configuration // 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解
@EnableSwagger2 // 开启 Swagger2
public class SwaggerConfig {
// 配置 Swagger 的 Docket Bean 实例
@Bean
public Docket docket(Environment environment) {
// 设置要 使用的 Swagger 环境
Profiles profiles = Profiles.of("dev", "test");
// 获取 监听 环境变量,判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// 配置 API文档 的分组
.groupName("尔尔")
// 配置 是否启动 Swagger
.enable(flag)
// 配置 扫描接口
.select()
/*
RequestHandlerSelectors:配置要扫描接口的方式
• basePackage("erer.springboot.swagger.controller"):基于某一个包进行扫描
• any():扫描全部
• none():都不扫描
• withClassAnnotation(Controller.class):扫描函用此注解的类,参数是一个注解的放射对象
• withMethodAnnotation(RequestMapping.class):扫描含有此注解的方法,参数是一个注解的放射对象
*/
.apis(RequestHandlerSelectors.withClassAnnotation(Controller.class))
// 过滤路径(PathSelectors.ant("/hello/**"):扫描 hello 的路径)
.paths(PathSelectors.ant("/hello/**"))
.build();
}
// 配置 Swagger 信息 apiInfo
public ApiInfo apiInfo() {
// 作者信息
Contact CONTACT = new Contact("尔尔", "", "313082141@qq.com");
return new ApiInfo(
"尔尔的 Swagger API 文档",
"最难不过坚持",
"1.0",
"http://localhost:8080/",
CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}
5. 设置分组
package erer.springboot.swagger.config;
@Configuration // 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解
@EnableSwagger2 // 开启 Swagger2
public class SwaggerConfig {
@Bean
public Docket docket01() {
return new Docket(DocumentationType.SWAGGER_2).groupName("a");
}
@Bean
public Docket docket02() {
return new Docket(DocumentationType.SWAGGER_2).groupName("b");
}
@Bean
public Docket docket03() {
return new Docket(DocumentationType.SWAGGER_2).groupName("c");
}
}
6. 加载注释
package erer.springboot.swagger.domain;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Api("注释")
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户id")
private int id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
}
六、多线程
1、实现方法
1. 继承 Thread
package com.erer.gulimall.search.thread;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
Thread thread = new Thread01();
thread.start();
System.out.println("main......end.....");
}
public static class Thread01 extends Thread {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
}
2. 实现 Runnable 接口
package com.erer.gulimall.search.thread;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();
System.out.println("main......end.....");
}
public static class Runnable01 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
}
3. 实现Callable接口
package com.erer.gulimall.search.thread;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
System.out.println("main......end.....");
// 等待整个线程执行完,获得返回结果
Integer result = futureTask.get();
System.out.println(result);
}
public static class Callable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}
}
}
4. 线程池
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
executor.execute(new Runnable01());
Future<Integer> submit = executor.submit(new Callable01());
System.out.println(submit.get());
}
public static class Runnable01 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
public static class Callable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}
}
}
2、线程池
1. 七大参数
public ThreadPoolExecutor(
int corePoolSize,//池中一直保持的线程的数量,即使线程空闲
int maximumPoolSize,//池中允许的最大的线程数
long keepAliveTime,//空闲线程存活时间,最终线程池维持在corePoolSize 大小
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列,线程的需求超过了corePoolSize,用来存储等待执行的任务
ThreadFactory threadFactory,//创建线程的工厂,比如指定线程名等
RejectedExecutionHandler handler//拒绝策略,如果线程满了,线程池就会使用拒绝策略。
)
2. 运行流程
- 线程池创建,准备好core 数量的核心线程,准备接受任务
- 新的任务进来,用core 准备好的空闲线程执行。
- core 满了,就将再进来的任务放入阻塞队列中,空闲的 core 就会自己去阻塞队列获取任务执行
- 阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
- max 都执行好了,Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁,最终保持到core 大小
- 如果线程数开到了max 的数量,还有新任务进来,就会使用reject 指定的拒绝策略进行处理
- 所有的线程创建都是由指定的factory 创建的。
3. 常见的线程池
- Executors.newCachedThreadPool():core 为0,所有都可回收
- Executors.newFixedThreadPool():固定大小,core=max
- Executors.newScheduledThreadPool():定时任务的线程池
- Executors.newSingleThreadExecutor():单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
3、CompletableFuture 异步编排
1. 创建异步对象
- runAsync
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
}
}
- supplyAsync:方法完成后可获取结果
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
Integer result = future.get();
System.out.println("result = " + result);
}
}
2. 回调方法
-
whenComplete 可以处理正常和异常的计算结果
- 执行当前任务的线程执行继续执行 whenComplete 的任务
-
exceptionally 处理异常情况。
- 执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res, exception) -> {
//虽然能得到异常信息,但是没法修改返回数据
System.out.println("异步任务成功完成了...结果是:" + res + "异常是:" + exception);
}).exceptionally(throwable -> {
//可以感知异常,同时返回默认值
return 10;
});
}
}
3. handle 方法
- 和complete 一样,可对结果做最后的处理(可处理异常),可改变返回值。
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((result, thr) -> {
if (result != null) {
return result * 2;
}
if (thr != null) {
System.out.println("异步任务成功完成了...结果是:" + result + "异常是:" + thr);
return 0;
}
return 0;
});
Integer result = future.get();
System.out.println("result = " + result); // 10
}
}
4. 线程串行化方法
- thenRunL:不能获取上一步的执行结果
- thenAcceptAsync:能接受上一步结果,但是无返回值
- thenApplyAsync:能接受上一步结果,有返回值
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("任务2启动了..." + res);
System.out.println("当前线程:" + Thread.currentThread().getId());
return "Hello" + res;
}, executor);
System.out.println("main......end....." + future.get());
}
}
5. 两任务组合
-
两个任务必须都完成,触发该任务:
- thenCombine:组合两个future,获取两个future 的返回结果,并返回当前任务的返回值
- thenAcceptBoth:组合两个future,获取两个future 任务的返回结果,然后处理任务,没有返回值
- runAfterBoth:组合两个future,不需要获取future 的结果,只需两个future 处理完任务后,
处理该任务。
-
当两个任务中,任意一个 future 任务完成的时候,执行任务:
- applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值
- acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值
- runAfterEither:两个任务有一个执行完成,不需要获取future 的结果,处理任务,也没有返
回值。
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("任务1结束:" + i);
return i;
}, executor);
CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
System.out.println("任务2结束:");
return "Hello";
}, executor);
future01.runAfterBothAsync(future02,
() -> System.out.println("任务3线程:" + Thread.currentThread().getId())
, executor);
}
}
6. 多任务组合
- allOf:等待所有任务完成
- anyOf:只要有一个任务完成
package com.erer.gulimall.search.thread;
public class ThreadTest {
// 当前系统中线程池只有一两个,每个异步任务均提交给线程池去执行
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品图片信息");
return "Hello.jpg";
}, executor);
CompletableFuture<String> futureArr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品属性");
return "黑色+256G";
}, executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品介绍");
return "华为";
}, executor);
CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureArr, futureDesc);
allOf.get(); //等待所有任务完成。
}
}
七、社交登录
1、引入依赖
<!-- feign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring-session-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- HttpUtils 工具相关依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.3.7.v20160115</version>
</dependency>
2、配置文件
1. application
spring:
# 配置 redis
redis:
host: 192.168.116.129
port: 6379
# 配置中心服务名称
application:
name: gulimall-auth-server
cloud:
# 配置 nacos 注册中心
nacos:
discovery:
# nacos 服务端地址
server-addr: localhost:8848
# 配置spring-session
session:
store-type: redis
2. bootstrap
spring:
application:
name: gulimall-auth-server # 服务名称
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
3、开启服务
package com.erer.gulimall.auth;
@SpringBootApplication
// 开启 nacos 注册中心服务
@EnableDiscoveryClient
// 开启远程调用服务
@EnableFeignClients(basePackages = "com.erer.gulimall.auth.feign")
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
4、配置 session
package com.erer.gulimall.auth.config;
@Configuration
// 开启 redis 作为 session 的存储
@EnableRedisHttpSession
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 放大 session 的作用域:默认发的令牌,仅作用于当前域
cookieSerializer.setDomainName("gulimall.com");
// 自定义 CookieName 令牌名称
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// 使用JSON的序列化方式来序列化对象到Redis中
return new GenericJackson2JsonRedisSerializer();
}
}
5、HttpUtils
package com.erer.common.utils;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
6、登录验证
package com.erer.gulimall.auth.controller;
@Slf4j
@Controller
public class OAuth2Controller {
@Resource
private MemberFeignService memberFeignService;
@GetMapping(value = "/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
Map<String, String> map = new HashMap<>();
map.put("client_id", "2077705774");
map.put("client_secret", "40af02bd1c7e435ba6a6e9cd3bf799fd");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
// 1、根据用户授权返回的code换取access_token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());
// 2、处理
if (response.getStatusLine().getStatusCode() == 200) {
// 获取到了access_token,转为通用社交登录对象
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
// 当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息,以后这个社交账号就对应指定的会员)
System.out.println(socialUser.getAccess_token());
// 调用远程服务
R oauthLogin = memberFeignService.oauthLogin(socialUser);
if (oauthLogin.getCode() == 0) {
MemberResponseVo data = oauthLogin.getData("data", new TypeReference<MemberResponseVo>() {
});
log.info("登录成功:用户信息:{}", data.toString());
// 第一次使用 session 命令浏览器保存卡号,JSESSIONID这个cookie,以后浏览器访问哪个网站就会带上这个网站的cookie
session.setAttribute(LOGIN_USER, data);
//2、登录成功跳回首页
return "redirect:http://gulimall.com";
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
}
}
7、远程调用
package com.erer.gulimall.auth.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping(value = "/member/member/oauth2/login")
R oauthLogin(@RequestBody SocialUser socialUser) throws Exception;
}
8、登录服务
- Controller
package com.erer.gulimall.member.controller;
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
// 社交登录
@PostMapping(value = "/oauth2/login")
public R oauthLogin(@RequestBody SocialUser socialUser) throws Exception {
MemberEntity memberEntity = memberService.login(socialUser);
if (memberEntity != null) {
return R.ok().setData(memberEntity);
} else {
return R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(),BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMessage());
}
}
}
- service
package com.erer.gulimall.member.service;
public interface MemberService extends IService<MemberEntity> {
/**
* 社交用户的登录
*/
MemberEntity login(SocialUser socialUser) throws Exception ;
}
- impl
package com.erer.gulimall.member.service.impl;
@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {
@Resource
private MemberLevelService memberLevelService;
@Override
public MemberEntity login(SocialUser socialUser) throws Exception {
System.out.println("socialUser" + socialUser.toString());
// 具有登录和注册逻辑
String uid = socialUser.getUid();
// 1、判断当前社交用户是否已经登录过系统
MemberEntity memberEntity = this.getOne(
new LambdaQueryWrapper<MemberEntity>()
.eq(MemberEntity::getSocialUid, uid)
);
if (memberEntity != null) {
// 用户已经注册过,更新用户的访问令牌的时间和access_token
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
// 没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity register = new MemberEntity();
try {
// 查询当前社交用户的社交账号信息(昵称、性别等)
Map<String, String> query = new HashMap<>();
query.put("access_token", socialUser.getAccess_token());
query.put("uid", socialUser.getUid());
// https://api.weibo.com/2/statuses/public_timeline.json?access_token=abcd
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
System.out.println(response.toString());
System.out.println("StatusCode = " + response.getStatusLine().getStatusCode());
if (response.getStatusLine().getStatusCode() == 200) {
// 查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
String profileImageUrl = jsonObject.getString("profile_image_url");
register.setNickname(name);
// 设置默认等级
MemberLevelEntity levelEntity = memberLevelService.getDefaultLevel();
register.setLevelId(levelEntity.getId());
register.setGender("m".equals(gender) ? 1 : 0);
register.setHeader(profileImageUrl);
register.setCreateTime(new Date());
}
} catch (Exception e) {
}
register.setSocialUid(socialUser.getUid());
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
// 把用户信息插入到数据库中
this.save(register);
return register;
}
}
}
八、消息队列
1、JMS规范
-
JMS(Java Message Service):一个规范,等同于JDBC规范,提供了与消息服务相关的API接口
-
JMS消息模型
- peer-2-peer:点对点模型,消息发送到一个队列中,队列保存消息。队列的消息只能被一个消费者消费,或超时
- publish-subscribe:发布订阅模型,消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在
-
JMS消息种类
- TextMessage
- MapMessage
- BytesMessage
- StreamMessage
- ObjectMessage
- Message (只有消息头和属性)
-
JMS实现:ActiveMQ、Redis、HornetMQ、RabbitMQ、RocketMQ(没有完全遵守JMS规范)
2、AMQP协议
-
AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS
-
优点:具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现
-
AMQP消息模型
- direct exchange
- fanout exchange
- topic exchange
- headers exchange
- system exchange
-
AMQP消息种类:byte[]
-
AMQP实现:RabbitMQ、StormMQ、RocketMQ
3、ActiveMQ
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
2. 配置参数
- 采用默认配置
spring:
activemq:
broker-url: tcp://localhost:61616
jms:
# 开启发布订阅模型(未开启采用点对点模型)
pub-sub-domain: true
template:
# 消息列队的名称 与 order.queue.id 保留一个即可
default-destination: erer
3. 生产消息
package erer.springboot.mq.service.impl.activeMQ;
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
private JmsMessagingTemplate messagingTemplate;
@Override
public void sendMessage(String id) {
System.out.println("代发短信的订单已纳入处理队列,id:" + id);
messagingTemplate.convertAndSend("order.queue.id", id);
}
@Override
public String doMessage() {
return messagingTemplate.receiveAndConvert("order.queue.id", String.class);
}
}
4. 消费消息
package erer.springboot.mq.listener;
@Component
public class MessageListener {
// 使用消息监听器对消息队列监听
@JmsListener(destination = "order.queue.id")
// 流程性业务消息消费完转入下一个消息队列
@SendTo("order.other.queue.id")
public String receive(String id) {
System.out.println("已完成短息发送业务,id:" + id);
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
return "new" + id;
}
}
5. 访问测试
package erer.springboot.mq.controller;
@RestController
@RequestMapping("/orders")
public class orderController {
@Autowired
private OrderService orderService;
@RequestMapping("/{id}")
public void order(@PathVariable String id) {
orderService.order(id);
}
}
4、RabbitMQ
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 配置参数
spring:
rabbitmq:
host: 192.168.116.129 # 主机名
port: 5672 # 端口号
virtual-host: / # 虚拟主机
username: admin # 用户名
password: admin # 密码
3. Direct
3.1 消息队列绑定交换机
package erer.springboot.mq.config;
@Configuration
@EnableRabbit // 开启RabbitMQ服务
public class RabbitDirectConfig {
@Bean // 创建消息队列:01
public Queue directQueue01() {
// durable:是否持久化,默认false
// exclusive:是否当前连接专用,默认false,连接关闭后队列即被删除
// autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除
return new Queue("erer.direct01"", true, false, false);
}
@Bean // 创建交换机:01
public DirectExchange directExchange() {
return new DirectExchange("directExchange");
}
@Bean // 交换机与消息队列01绑定
public Binding bindingDirect01() {
return BindingBuilder.bind(directQueue01()).to(directExchange()).with("direct");
}
@Bean // 创建消息队列:02
public Queue directQueue02() {
return new Queue("erer.direct02");
}
@Bean // 创建交换机02
public FanoutExchange fanoutExchange(){
return new FanoutExchange("erer.fanout");
}
@Bean // 交换机与消息队列02绑定
public Binding bindingDirect02(Queue directQueue02, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(directQueue02).to(fanoutExchange);
}
}
3.2 生产消息
package erer.springboot.mq.service.impl.rabbitMQ.direct;
@Service
public class MessageServiceRabbitMQImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String id) {
System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:" + id);
// 交换机名称、路由密钥(routingKey)、消息
amqpTemplate.convertAndSend("directExchange", "direct", id);
}
}
3.3 消费消息
- 多个 Listener 可轮询处理
package erer.springboot.mq.listener;
@Component
public class MessageListener {
@RabbitListener(queues = "erer.direct01")
public void received(String id) {
System.out.println("已完成短息发送业务(RabbitMQ.direct),id:" + id);
}
}
4. Topic
4.1 消息队列绑定交换机
package erer.springboot.mq.config;
@Configuration
public class RabbitTopicConfig {
@Bean // 创建交换机
public TopicExchange topicExchange() {
return new TopicExchange("topicExchange");
}
@Bean // 创建消息队列:01
public Queue topicQueue01() {
return new Queue("erer.topic01");
}
@Bean // 交换机与消息队列01绑定
public Binding bindingDirect01() {
return BindingBuilder.bind(topicQueue01()).to(topicExchange()).with("topic.*.id");
}
@Bean // 创建消息队列:02
public Queue topicQueue02() {
return new Queue("erer.topic02");
}
@Bean // 交换机与消息队列02绑定
public Binding bindingDirect02() {
return BindingBuilder.bind(topicQueue02()).to(topicExchange()).with("topic.#");
}
}
4.2 生产消息
package erer.springboot.mq.service.impl.rabbitMQ.topic;
@Service
public class MessageServiceRabbitMQImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String id) {
System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:" + id);
// 交换机名称、路由密钥(routingKey)、消息
amqpTemplate.convertAndSend("topicExchange", "topic.order.id", id);
}
}
绑定键匹配规则:
- * (星号): 用来表示一个单词 ,且该单词是必须出现的
- # (井号): 用来表示任意数量
匹配键 | topic.*.* | topic.# |
---|---|---|
topic.order.id | true | true |
order.topic.id | false | false |
topic.sm.order.id | false | true |
topic.sm.id | false | true |
topic.id.order | true | true |
topic.id | false | true |
topic.order | false | true |
4.3 消费消息
- 当消息传递匹配多个绑定键(routingKey),消息会同时发布到所对应的多个消息队列
package erer.springboot.mq.listener;
@Component
public class MessageListener {
@RabbitListener(queues = "erer.topic01")
public void received01(String id) {
System.out.println("已完成短息发送业务(RabbitMQ.topic01),id:" + id);
}
@RabbitListener(queues = "erer.topic02")
public void received02(String id) {
System.out.println("已完成短息发送业务(RabbitMQ.topic02),id:" + id);
}
}
5、RocketMQ
1. 引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
2. 配置参数
rocketmq:
name-server: localhost:9876
producer:
group: group_rocketmq
3. 生产消息
package erer.springboot.mq.service.impl.rocketMQ;
@Service
public class MessageServiceRocketMQImpl implements MessageService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
public void sendMessage(String id) {
// 同步消息
// rocketMQTemplate.convertAndSend("order_id", id);
// System.out.println("使用Rocketmq将待发送短信的订单纳入处理队列,id:" + id);
// 异步消息
SendCallback callback = new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("消息发送成功!");
}
@Override
public void onException(Throwable throwable) {
System.out.println("消息发送失败!");
}
};
System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:" + id);
rocketMQTemplate.asyncSend("order_id", id, callback);
}
}
4. 消息消费
package erer.springboot.mq.listener;
@Component
@RocketMQMessageListener(topic = "order_id", consumerGroup = "group_rocketmq")
public class RocketmqMessageListener implements RocketMQListener<String> {
@Override
public void onMessage(String id) {
System.out.println("已完成短信发送业务,id:" + id);
}
}
6、Kafka
1. 启动Kafka
1.1 启动zookeeper
- 默认端口:2181
zookeeper-server-start.bat ..\..\config\zookeeper.properties
1.2 启动kafka
- 默认端口:9092
kafka-server-start.bat ..\..\config\server.properties
1.3 创建topic
# 创建topic
kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic erer
# 查看topic
kafka-topics.bat --zookeeper 127.0.0.1:2181 --list
# 删除topic
kafka-topics.bat --delete --zookeeper localhost:2181 --topic erer
1.4 生产者功能测试
kafka-console-producer.bat --broker-list localhost:9092 --topic erer
1.5 消费者功能测试
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic erer --from-beginning
2. 引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
3. 配置参数
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: order
4. 生产消息
package erer.springboot.mq.service.impl.kafka;
@Service
public class MessageServiceKafkaImpl implements MessageService {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Override
public void sendMessage(String id) {
System.out.println("代发短信的订单已纳入处理队列,id:" + id);
kafkaTemplate.send("erer", id);
}
}
5. 消费消息
package erer.springboot.mq.listener;
@Component
public class MessageListener {
@KafkaListener(topics = {"erer"})
public void received(ConsumerRecord<?, ?> record) {
System.out.println("已完成短信发送业务(kafka),id:"+record.value());
}
}
九、SpringBoot监控
- Spring Boot Admin 是一个开源社区项目,用于管理和监控 SpringBoot 应用程序
- Spring Boot Admin 有两个角色,客户端 (Client) 和服务端 (Server)
- 应用程序作为 Spring Boot Admin Client 向为 Spring Boot Admin Server 注册
- Spring Boot Admin Server 的 UI 界面将 Spring Boot Admin Client 的 Actuator Endpoint 上的一些监控信息。
1、admin-server
1. 引入依赖
- Ops --> Spring Boot Admin(Server)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.5.4</version>
</dependency>
2. 启用监控
package erer.springboot.admin;
// 启用监控
@EnableAdminServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 配置参数
server:
port: 9000
2、admin-client
1. 引入依赖
Ops --> Spring Boot Admin(Client)
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.5.4</version>
</dependency>
2. 配置参数
# 为info端点添加自定义指标
info:
# 获取 pom 文件信息
appName: @project.artifactId@
version: @project.version@
author: erer
server:
port: 80
spring:
boot:
admin:
client:
# 监控服务所在端口
url: http://localhost:9000
management:
# 将所有的监控 endpoint 暴露出来
endpoints:
web:
exposure:
# 端点功能暴露 "*":暴露所有、"health, info":对应部分暴露
include: "*"
# 控制全部端点功能开启与关闭
enabled-by-default: true
# 开启健康检查的完整信息
endpoint:
health:
show-details: always
info:
# 控制对应端点功能开启与关闭
enabled: true
3. info 添加自定义指标
import java.util.Map;
@Component
public class InfoConfig implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
Map<String, Object> infoMap = new HashMap<>();
infoMap.put("buildTime", "2006");
builder.withDetail("runTime", System.currentTimeMillis())
.withDetail("company", "尔尔");
builder.withDetails(infoMap);
}
}
4. 自定义端点
package erer.springboot.actuator.actuator;
@Component
@Endpoint(id = "pay", enableByDefault = true)
public class PayEndPoint {
@ReadOperation
public Object getPay() {
//调用业务操作,获取支付相关信息结果,最终return出去
Map<String, Integer> payMap = new HashMap<>();
payMap.put("level 1", 103);
payMap.put("level 2", 315);
payMap.put("level 3", 666);
return payMap;
}
}
3、启动服务
- 启动 admin-client
- 启动 admin-server
- 访问 http://localhost:9000/applications
4、监控原理
-
Actuator提供了SpringBoot生产就绪功能,通过端点的配置与访问,获取端点信息
-
端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息
-
访问当前应用所有端点信息:/actuator
-
访问端点详细信息:/actuator/端点名称
- /beans 描述应用程序上下文里全部的 Bean,以及它们的关系、
- /env 获取全部环境属性
- /env/{name} 根据名称获取特定的环境属性值
- /health 报告应用程序的健康指标,这些值由 HealthIndicator 的实现类提供
- /info 获取应用程序的定制信息,这些信息由 info 打头的属性提供
- /mappings 描述全部的 URI 路径,以及它们和控制器(包含 Actuator 端点)的映射关系
- /metrics 报告各种应用程序度量信息,比如内存用量和 HTTP 请求计数
- /metrics/{name} 报告指定名称的应用程序度量值
- /trace 提供基本的 HTTP 请求跟踪信息(时间戳、HTTP 头等)
- /loggers 显示和修改应用程序中日志记录器的配置