Minio
客户端使用
-
对象(Object)
对象是实际的数据单元,例如我们上传的一个图片。
-
存储桶(Bucket)
存储桶是用于组织对象的命名空间,类似于文件夹。每个存储桶可以包含多个对象。(相当于文件夹)
-
端点(Endpoint)
端点是MinIO服务器的网络地址,用于访问存储桶和对象,例如
http://192.168.10.101:9000
注意:
9000
为MinIO的API的默认端口,前边配置的9001
以为管理页面端口。
根据前面的设置,登入进来创建桶
点这里 创建,输入完你要创建的桶的名字后点确认
上传图片(点击我们刚刚创建的桶,我这里创建的名称为demo1)
进来后是这个样子,点击upload上传文件
可以直接从网页打开图片 ,minio的ip+同的名称+文件全名
举例我这里就是
192.168.17.101:9000/demo1/t.jgp
没有访问成功
回到这里,看我们桶的名称这里,是private私有的,无法访问,所以需要设置
这里选择custom
然后将复制上去,这里要设置自己的桶名称,我这里是demo1
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::demo1/*"
]
}
]
}
重新尝试一下
在java中使用minio
引入依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.3</version>
</dependency>
编写如下类
package org.example;
import io.minio.*;
import io.minio.errors.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TestMinio {
public static void main(String[] args) {
String ip = "http://192.168.17.101:9000"; //设置路径
String accessKey = "minioadmin"; //用户名
String secretKey = "minioadmin"; //密码
String bucketName = "test"; //存储桶名称
MinioClient minioClient = MinioClient.builder().credentials(accessKey, secretKey).endpoint(ip).build();//将账户密码还有ip放进去,获得客户端的minio
try {
boolean b = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); //判断桶是否存在
if (b) {
} else{
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); //创建桶
String policy= """
{
"Statement" : [ {
"Action" : "s3:GetObject",
"Effect" : "Allow",
"Principal" : "*",
"Resource" : "arn:aws:s3:::%s/*"
} ],
"Version" : "2012-10-17"
}
""".formatted(bucketName); //json串中的%s是占位符,用formatted来放进去
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(policy).build()); //设置桶的访问权限
}
//filename是本地文件路径,bucket是桶的名称,object是桶中的对象名称,也可以成为桶中文件的名称
String filename="D:\\java\\beijing//1.png";
minioClient.uploadObject(UploadObjectArgs.builder().filename(filename).bucket(bucketName).object("2.png").build()); //上传文件
System.out.println("上传成功");
} catch (Exception e) {
System.out.println("失败");
e.printStackTrace();
}
}
}
Redis
说明
Redis(Remote Dictionary Server)是一个基于内存的键值对存储系统,常用作缓存服务。由于Reids将数据都保存在内存中,因此其读写性能十分惊人,同时,为保证数据的可靠性,Redis会将数据备份到硬盘上,用于故障发生时的数据恢复
Redis特点
高性能:Redis主要将数据存储在内存中,因此读写速度非常快,适合对速度有较高要求的场景。
支持多种数据结构:Redis中键值对的值(Value)支持多种数据结构,如字符串、哈希表、列表、集合等,这使得它可以应用于多种不同的场景。
持久化:Redis可以通过定期快照或者实时记录写操作日志的方式将内存中的数据持久化到硬盘,确保数据在重启后不会丢失。
灵活的数据过期策略:可以为每个键设置过期时间,一旦过期,Redis会自动删除
Redis最为常见的一个应用场景就是用作缓存,缓存可以显著提升访问速度,降低数据库压力。
Redis常用数据类型及命令
通用命令
查看所有键
keys 命令可用于查看所有键,语法如下
keys pattern
pattern用于匹配key,其中
*
表示任意个任意字符,?
表示一个任意字符。127.0.0.1:6379> KEYS *
1) "k3"
2) "k2"
3) "k1"注意:该命令会遍历Redis服务器中保存的所有键,因此当键很多时会影响整个Redis服务的性能,线上环境需要谨慎使用。
键总数
dbsize
可用于查看键的总数,语法如下dbsize
判断键是否存在
exists
命令可用于判断一个键是否存在,语法如下exists key
说明:若键存在则返回1,不存在则返回0。
删除键
del
可用于删除指定键,语法如下del key [key ...]
说明:返回值为删除键的个数,若删除一个不存在的键,则返回0。
查询键的剩余过期时间
ttl key
说明:
ttl
的含义为time to live,用于查询一个定时键的剩余存活时间,返回值以秒为单位。若查询的键的未设置过期时间,则返回-1
,若查询的键不存在,则返回-2
。
数据库管理命令
Redis默认有编号为0~15的16个逻辑数据库,每个数据库之间的数据是相互独立的,所有连接默认使用的都是0号数据库。
切换数据库
select
命令可用于切换数据库,语法如下select index
说明:若index超出范围,会报错
清空数据库
flushdb
命令会清空当前所选用的数据库
flushall
命令会清空0~15号所有的数据库。建议谨慎使用
String类型
Redis中的string类型保存的是字节序列(Sequence of bytes),因此任意类型的数据,只要经过序列化之后都可以保存到Redis的string类型中,包括文本、数字甚至是一个对象。
常用命令
set :
set
命令用于添加string类型的键值对,具体语法如下SET key value [NX|XX] [EX seconds|PX milliseconds]
各选项含义如下
NX:仅在key不存在时set
XX:仅在key存在时set
EX seconds:设置过期时间,单位为秒
PX milliseconds:设置过期时间,单位为毫秒
get:
get
命令用于获取某个string类型的键对应的值,具体语法如下get key
incr
incr
命令用于对数值做自增操作,具体语法如下INCR key
decr :
decr
命令用于对数值做自减操作,具体语法如下。DECR key
若key对应的value是整数,则返回自减后的结果,若不是整数则报错,若key不存在则创建并返回-1。
应用场景
string类型常用于缓存、计数器等场景。
list类型
list类型可用于存储多个string类型的元素,并且所有元素按照被添加的顺序存储
常用命令
lpush s2 a b c d
栈,先进后出
rpush
该命令用于向list右侧添加元素,语法如下
rpush key element [element ...]
linsert
该命令用于向list指定位置添加元素,语法如下
在 pivot这个元素之前|之后,添加element这个元素
linsert key before|after pivot element
查询元素
查询list元素的命令有lindex
和lrange
,各命令的功能与用法如下
lindex
该命令用于获取指定索引位置的元素,语法如下
lindex key index
lrange
该命令用于获取指定范围内的元素列表,语法如下
lrange key start stop
获取list全部元素,命令如下
lrange l1 0 -1
删除元素
删除list元素的命令有lpop
、rpop
、lrem
,各命令的功能与用法如下
lpop
该命令用于移除并返回list左侧元素,语法如下
lpop key
rpop
该命令用于移除并返回list右侧的元素,语法如下
rpop key
lrem
该命令用于移除list中的指定元素,语法如下
lrem key count element
说明:count参数表示要移除element元素的个数(list中可以存在多个相同的元素),count的用法如下
若count>0,则从左到右删除最多count个element元素
若count<0,则从右到左删除最多count(的绝对值)个element元素
若count=0,则删除所有的element元素
修改元素
lset
命令可用于修改指定索引位置的元素,语法如下lset key index element
其他
llen
命令可用于查看list长度,语法如下llen key
set类型
和list类型相似,set类型也可用来存储多个string类型的元素,但与list类型不同,set中的元素是无序的,且set中不会包含相同元素。
添加
sadd key member [member ...]
于查询set中的全部元素
smembers key
该命令用于移除set中的指定元素
srem key member [member ...]
该命令随机移除1个元素并返回set中移出的那个元素
spop key
该命令随机返回set中的n个元素(不删除)
srandmember key [count]
该命令用于查询set中的元素个数,语法如下
scard key
集合间
该命令用于元素是否在set中
sismember key element
该命令用于计算多个集合的交集,语法如下
sinter key [key ...]
该命令用于计算多个集合的并集,语法如下
sunion key [key ...]
该命令用于计算多个集合的差集
sdiff key [key ...]
set可用于计算共同关注好友,随机抽奖系统等等。
hash类型
hash类型类似于Java语言中的HashMap
,可用于存储键值对。
常用命令
hset
该命令用于向hash中增加键值对,语法如下(我试了一下,我这里只能一个一个加。不能同时加好几个,不清楚原因)
hset key field value [field value ...]
hget
该命令用于获取hash中某个键对应的值,语法如下
hget key field
hdel
该命令用于删除hash中的指定的键值对,语法如下
hdel key field [field ...]
hexists
该命令用于判断hash中的某个键是否存在,语法如下
hexists key field
hkeys
该命令用于返回hash中所有的键,语法如下
hkeys key
hvals
该命令用于返回hash中所有的值,语法如下
hvals key
hgetall
该命令用于返回hash中所有的键与值,语法如下
hgetall key
应用场景 :hash类型可用于缓存对象等。
zset类型
zset(sorted set)被称为有序集合,同set相似,zset中也不会包含相同元素,但不同的是,zset中的元素是有序的。并且zset中的元素并非像list一样按照元素的插入顺序排序,而是按照每个元素的分数(score)排序。
zadd
该命令用于向zset中添加元素,语法如下
ZADD key [NX|XX] score member
说明:
NX:仅当member不存在时才add
XX:仅当member存在时才add
zcard
该命令用于计算zset中的元素个数,语法如下
zcard key
zscore
改名用于查看某个元素的分数,语法如下
zscore key member
zrank/zrevrank
这组命令用于计算元素的排名,其中zrank按照score的升序排序,zrevrank则按照降序排序,语法如下
zrank/zrevrank key member
说明:名次从0开始。
zrem
该命令用于删除元素,语法如下
zrem key member [member ...]
zincrby
该命令用于增加元素的分数,语法如下
zincrby key increment member
zrange
该命令用于查询指定区间范围的元素,语法如下
zrange key start stop [byscore] [rev] [limit offset count] [withscores]
说明:
start/stop:用于指定查询区间,但是在不同模式下,其代表的含义也不相同
默认模式下,
start~stop
表示的是名次区间,且该区间为闭区间。名次从0开始,且可为负数,-1表示倒数第一,-2表示倒数第二,以此类推。byscore模式下(声明了byscore参数),则
start~stop
表示的就是分数区间,该区间默认仍为闭区间。在该模式下,可以在start
或stop
前增加(
来表示开区间,例如(1 (5
,表示的就是(1,5)
这个开区间。除此之外,还可以使用-inf
和+inf
表示负无穷和正无穷。byscore:用于切换到分数模式
rev:表示降序排序。在byscore模式下使用rev参数需要注意查询区间,start应大于stop。
limit:该选项只用于byscore模式,作用和sql语句中的limit一致
withscores:用于打印分数
应用场景
zset主要用于各种排行榜。
SpringBoot整合Redis
Spring Data Redis 是Spring大家族中的一个子项目,主要用于Spring程序和Redis的交互。它基于的Redis Java客户端(Jedis和Lettuce)做了抽象,提供了一个统一的编程模型,使得Spring程序与Redis的交互变得十分简单。
Spring Data Redis 中有一个十分重要的类——RedisTemplate
,它封装了与Redis进行的交互的各种方法,我们主要用使用它与Redis进行交互。
Spring Data Redis快速入门
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
编写application.yml文件
spring:
data:
redis:
host: 192.168.17.101
port: 6379
database: 0 #使用0号数据库
RedisTemplate使用
由于spring-boot-starter-data-redis
中提供了RedisTemplate
的自动配置,所以我们可以将RedisTemplate
注入自己的类中,如下边的案例所示
@SpringBootTest
public class TestRedisTemplate {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate() {
}
}
分组 | 说明 |
---|---|
redisTemplate.opsForValue() | 操作string类型的方法 |
redisTemplate.opsForList() | 操作list类型的方法 |
redisTemplate.opsForSet() | 操作set类型的方法 |
redisTemplate.opsForHash() | 操作hash类型的方法 |
redisTemplate.opsForZSet() | 操作zset类型的方法 |
redisTemplate | 通用方法 |
下面简单测试几个简单的方法
@SpringBootTest
public class TestRedisTemplate {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testSet() {
redisTemplate.opsForValue().set("key1", "value1");
}
@Test
public void testGet() {
String result = (String) redisTemplate.opsForValue().get("key1");
System.out.println(result);
}
@Test
public void testDel() {
redisTemplate.delete("key1");
}
插入成功,可以看出这里显示的不正常
上述问题的根本原因是,Redis中的key和value均是以二进制的形式存储的,因此客户端输入的key和value都会经过序列化之后才发往Redis服务端。而RedisTemplate所使用序列化方式和命令行客户端采用序列化方式不相同,进而导致序列化之后的二进制数据不同,所以才会导致上述的现象。
解决方式
StringRedisTemplate使用
为解决上述问题,可使用StringRedisTemplate
代替RedisTemplate
,因为StringRedisTemplate
使用的序列化器和命令行所使用的序列化器是相同的。
spring-boot-starter-data-redis
同样提供了StringRedisTemplate
的自动配置,因此我们也可以直接将其注入到自己的类中。实例代码如下
@SpringBootTest(classes = Main.class)
public class RedisTest {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void test(){
ValueOperations value = redisTemplate.opsForValue();//Sting
value.set("key1","value");
value.set("age","18");
}
}
Knife4j
Knife4j是一个用于生成和展示API文档的工具,同时它还提供了在线调试的功能,下图是其工作界面。
-
Knife4j有多个版本,最新版的Knife4j基于开源项目
springdoc-openapi
,这个开源项目的核心功能就是根据SpringBoot项目中的代码自动生成符合OpenAPI规范的接口信息。 -
OpenAPI规范定义接口文档的内容和格式,其前身是
Swagger
规范。
与SpringBoot集成
引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
编写配置类
@Configuration
public class Knife4jConfiguration {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("hello-knife4j项目API")
.version("1.0")
.description("hello-knife4j项目的接口文档"));
}
@Bean
public GroupedOpenApi userAPI() {
return GroupedOpenApi.builder().group("用户信息管理").
pathsToMatch("/user/**").
build();
}
@Bean
public GroupedOpenApi systemAPI() {
return GroupedOpenApi.builder().group("产品信息管理").
pathsToMatch("/product/**").
build();
}
}
编写实体类
@Data
@Schema(description = "用户信息实体")
public class User {
@Schema(description = "编号")
private Long id;
@Schema(description = "用户姓名")
private String name;
@Schema(description = "用户年龄")
private Integer age;
@Schema(description = "用户邮箱")
private String email;
}
@Schema
注解用于描述作为接口参数或者返回值的实体类的数据结构。
描述Controller接口
@RestController
@RequestMapping("/user")
@Tag(name = "用户信息管理")
public class HelloController {
@Operation(summary = "根据id获取用户信息")
@GetMapping("getById")
public User getUserById(@Parameter(description = "用户id") @RequestParam Long id) {
User user = new User();
user.setId(id);
user.setName("zhangsan");
user.setAge(11);
user.setEmail("zhangsan@email.com");
return user;
}
}
@Tag
注解用于对接口进行分类,相同Tag
的接口会放在同一个菜单。
@Operation
用于对接口进行描述。
@Parameter
用于对HTTP请求参数进行描述
启动SpringBoot项目,访问http://localhost:8080/doc.html,观察接口文档。
收获与技巧
1.统一处理数据库公共字段的增加与修改
解决方式:创建一个类,类中属性为数据库的公共字段.,所有的数据库对应的类都继承这个类,这些类中无需编写公共字段属性
@JsonIgnore 设置不响应
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") //设置响应格式
@TableField(value = "create_time", fill = FieldFill.INSERT) 插入的时候自动生效
@TableField(value = "update_time",fill = FieldFill.UPDATE) 修改的时候自动生效
@Data
public class BaseEntity implements Serializable {
@Schema(description = "主键")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "创建时间")
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonIgnore
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@Schema(description = "更新时间")
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") //设置响应格式
@JsonIgnore
@TableField(value = "update_time",fill = FieldFill.UPDATE)
private Date updateTime;
@TableLogic //逻辑删除
@Schema(description = "逻辑删除")
@JsonIgnore //设置这个属性不响应
@TableField(value = "is_deleted")
private Byte isDeleted;
}
2.创建一个配置自动填充的内
这里是 插入的时候填充 creaetime的值
修改的时候填充 updatetime的值
metaObject.setValue("属性名称", 要插入的值“);
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", new Date());
}
}
枚举前后端处理
枚举字段在数据库、实体类、前后端交互的过程中有多种不同的形式,因此在请求和响应的过程中,枚举字段会涉及到多次类型转换。
数据库中的type字段为tinyint
类型
实体类
实体类中的type字段为ItemType
枚举类型
ItemType
枚举类如下
前后端交互中
前后端交互所传递的数据中type字段为数字(1/2)。
具体转换过程如下图所示:
请求流程
SpringMVC中的
WebDataBinder
组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。Mybatis中的
TypeHandler
用于处理Java中的实体对象与数据库之间的数据类型转换。
响应流程
SpringMVC中的
HTTPMessageConverter
组件负责将Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象),例如下一个接口保存或更新标签信息
解决方式
WebDataBinder枚举类型转换
WebDataBinder
依赖于Converter实现类型转换,若Controller方法声明的@RequestParam
参数的类型不是String
,WebDataBinder
就会自动进行数据类型转换。SpringMVC提供了常用类型的转换器,例如String
到Integer
、String
到Date
,String
到Boolean
等等,其中也包括String
到枚举类型,但是String
到枚举类型的默认转换规则是根据实例名称("APARTMENT")转换为枚举对象实例(ItemType.APARTMENT)。若想实现code
属性到枚举对象实例的转换,需要自定义Converter
,代码如下,具体内容可参考官方文档。
单个枚举解决
转化器
@Component
public class StringToItemTypeConverter implements Converter<String, ItemType> {
@Override
public ItemType convert(String code) {
for (ItemType value : ItemType.values()) {
if (value.getCode().equals(Integer.valueOf(code))) {
return value;
}
}
throw new IllegalArgumentException("code非法");
}
}
注册
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
private StringToItemTypeConverter stringToItemTypeConverter;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(this.stringToItemTypeConverter);
}
}
问题二+解决(推荐):
但是我们有很多的枚举类型都需要考虑类型转换这个问题,按照上述思路,我们需要为每个枚举类型都定义一个Converter,并且每个Converter的转换逻辑都完全相同,针对这种情况,我们使用ConverterFactory接口更为合适,这个接口可以将同一个转换逻辑应用到一个接口的所有实现类,因此我们可以定义一个
BaseEnum
接口,然后另所有的枚举类都实现该接口,然后就可以自定义ConverterFactory
,集中编写各枚举类的转换逻辑了。具体实现如下:public interface BaseEnum { Integer getCode(); String getName(); }
创建一个所有继承BaseEnum的转化器
@Component public class StringBaseEnumToitemTypeConverter implements ConverterFactory<String, BaseEnum> { @Override public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) { return new Converter<String, T>() { //内部重写方法,直接返回 @Override public T convert(String source) { T[] ts = targetType.getEnumConstants(); //获取所有targetType的枚举值 for (T t : ts) { if (t.getCode().equals(Integer.parseInt(source))) return t; } throw new IllegalArgumentException("code非法"); } }; }
注册
@Configuration public class WebMvcConfiguration implements WebMvcConfigurer { @Autowired private StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory; @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(this.stringToBaseEnumConverterFactory); } }
TypeHandler枚举类型转换
Mybatis预置的
TypeHandler
可以处理常用的数据类型转换,例如String
、Integer
、Date
等等,其中也包含枚举类型,但是枚举类型的默认转换规则是枚举对象实例(ItemType.APARTMENT)和实例名称("APARTMENT")相互映射。若想实现code
属性到枚举对象实例的相互映射,需要自定义TypeHandler
。不过MybatisPlus提供了一个通用的处理枚举类型的TypeHandler。其使用十分简单,只需在
ItemType
枚举类的code
属性上增加一个注解@EnumValue
,Mybatis-Plus便可完成从ItemType
对象到code
属性之间的相互映射,具体配置如下。
public enum ItemType {
APARTMENT(1, "公寓"),
ROOM(2, "房间");
@EnumValue
private Integer code;
private String name;
ItemType(Integer code, String name) {
this.code = code;
this.name = name;
}
}
HTTPMessageConverter枚举类型转换
HttpMessageConverter
依赖于Json序列化框架(默认使用Jackson)。其对枚举类型的默认处理规则也是枚举对象实例(ItemType.APARTMENT)和实例名称("APARTMENT")相互映射。不过其提供了一个注解@JsonValue
,同样只需在ItemType
枚举类的code
属性上增加一个注解@JsonValue
,Jackson便可完成从ItemType
对象到code
属性之间的互相映射。具体配置如下,详细信息可参考Jackson官方文档。
public enum ItemType implements BaseEnum {
APARTMENT(1, "公寓"),
ROOM(2, "房间");
@EnumValue //Mybatis-Plus完成从`ItemType`对象到`code`属性之间的相互映射
@JsonValue //Jackson完成从`ItemType`对象到`code`属性之间的相互映射
private Integer code;
private String name;
Minio图片上传
图片上传流程
可以看出图片上传接口接收的是图片文件,返回的Minio对象的URL
引入依赖(这里版本号再父工程里)
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
配置Minio相关参数
#minio配置
minio:
endpoint: http://192.168.17.101:9000 # minio服务器地址
accessKey: minioadmin # 用户名
secretKey: minioadmin # 密钥
bucketName: lease #桶名称
创建一个类来结束这些参数
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
}
将MinioClient加入容器
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfiguration {
@Autowired
private MinioProperties properties;
@Bean
public MinioClient minioClient() {
return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build();
}
}
上传图片代码
@Service
public class FileServiceImpl implements FileService {
@Autowired
private MinioClient client;
@Autowired
private MinioProperties properties;
@Override
public String upload(MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
boolean b = client.bucketExists(BucketExistsArgs.builder().bucket(properties.getBucketName()).build()); //判断桶是否存在
if (!b) {
//创建桶
client.makeBucket(MakeBucketArgs.builder().bucket(properties.getBucketName()).build());
//设置桶的权限,只允许自己写,其他人只有读的权限
String policy = policys(properties.getBucketName());
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(properties.getBucketName()).config(policy).build());
}
//上传文件
InputStream inputStream = file.getInputStream(); //获取流
long size = file.getSize();//文件大小
UUID uuid = UUID.randomUUID(); //获取一个随机的id
String filename = new SimpleDateFormat("yyyyMMdd").format(new Date()) + // "/"之前是一个一级目录
"/" + uuid + "-" + file.getOriginalFilename(); //创建一个独一无二的文件名称
client.putObject(PutObjectArgs.builder()
.bucket(properties.getBucketName())
.stream(inputStream, size, -1)//如果文件大小已知,那最后一项就填-1
.object(filename)
.contentType("image/png") //设置响应文件类型
.build());
//方式一 String.join("/", properties.getEndpoint(), properties.getBucketName(), filename);
//join 第一个位置是分隔符, 第二个位置是可变数组
// String url=properties.getEndpoint()+"/"+properties.getBucketName()+"/"+filename; //方式二
return String.join("/", properties.getEndpoint(), properties.getBucketName(), filename);
}
public String policys(String bucketName) {
String policy = """
{
"Statement" : [ {
"Action" : "s3:GetObject",
"Effect" : "Allow",
"Principal" : "*",
"Resource" : "arn:aws:s3:::%s/*"
} ],
"Version" : "2012-10-17"
}
""".formatted(bucketName);
return policy;
}
全局异常处理
编写自定义异常
@Data
public class LeaseException extends RuntimeException{
//异常状态码
private Integer code;
/**
* 通过状态码和错误消息创建异常对象
* @param message
* @param code
*/
public LeaseException(String message, Integer code) {
super(message);
this.code = code;
}
/**
* 根据响应结果枚举对象创建异常对象
* @param resultCodeEnum
*/
public LeaseException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
}
@Override
public String toString() {
return "LeaseException{" +
"code=" + code +
", message=" + this.getMessage() +
'}';
}
}
全局异常处理
//全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理自定义异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e)
{
e.printStackTrace();
return Result.fail();
}
@ExceptionHandler(LeaseException.class)
public Result error(LeaseException e){
e.printStackTrace();
return Result.fail(e.getCode(), e.getMessage());
}
}
Knife4j打平处理
默认情况下Knife4j为该接口生成的接口文档如下图所示,其中的queryVo参数不方便调试
可在application.yml文件中增加如下配置,将queryVo做打平处理
springdoc:
default-flat-param-object: true
Token学习
我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。
JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由
.
分隔。三个部分分别被称为
header
(头部)
payload
(负载)
signature
(签名)
各部分的作用如下
Header(头部)
Header部分是由一个JSON对象经过
base64url
编码得到的,这个JSON对象用于保存JWT 的类型(typ
)、签名算法(alg
)等元信息,例如{ "alg": "HS256", "typ": "JWT" }
Payload(负载)
也称为 Claims(声明),也是由一个JSON对象经过
base64url
编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除此之外,我们还可以自定义任何字段,例如
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
Signature(签名)
由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。
登入流程
所需依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
创建JWT工具类
public class JwtUtil {
private static long tokenExpiration = 60 * 60 * 1000L;
private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());
//生成token
public static String createToken(Long userId, String username) {
String token = Jwts.builder().
setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).
setSubject("LOGIN_USER").
claim("userId", userId).
claim("username", username).
signWith(tokenSignKey, SignatureAlgorithm.HS256).
compact();
return token;
}
//解析token,看是否过期,是否合法
public static Claims parseToken(String token) {
try {
JwtParser parser = Jwts.parserBuilder().setSigningKey(tokenSignKey).build();
Jws<Claims> claimsJws = parser.parseClaimsJws(token);
return claimsJws.getBody();
} catch (ExpiredJwtException e) { //过期异常
throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
} catch (JwtException e) { //其他异常
throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
}
}
拦截器
获取当前线程信息(这里LoginUser是一个实体类,里面只有id和用户名两个属性,不展示了)
public class LoginUserHolder {
private static ThreadLocal<LoginUser> threadLocal=new ThreadLocal();
public static void setLoginUser(LoginUser loginUser) {
threadLocal.set(loginUser);
}
public static LoginUser getLoginUser() {
return threadLocal.get();
}
public static void clear() {
threadLocal.remove();
}
}
拦截器
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("access-token");
if(token==null){
throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
}
Claims claims = JwtUtil.parseToken(token);
//将用户信息放入ThreadLocal中,在获取登陆用户个人信息中会用到
Long l = claims.get("userId", Long.class);
String s = claims.get("username", String.class);
LoginUser user = new LoginUser(l, s);
LoginUserHolder.setLoginUser(user);
return true;
}
//请求处理完,清理
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LoginUserHolder.clear();
}
}
图形验证码
本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档。
依赖
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>
@Override
public CaptchaVo getCaptcha() {
SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4); //设置验证码长度(宽高),最后一位时验证码长度,我这里设置的是验证码长度为4
String verCode = specCaptcha.text().toLowerCase(); //验证码
// String key = "admin:login"+UUID.randomUUID(); //生成唯一id,并设置它的前缀为admin:login
String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID(); //生成唯一id,并设置它的前缀为admin:login ,上一个的优化版
redisTemplate.opsForValue().set(key, verCode, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS); //将验证码存入redis,设置过期时间为60秒
return new CaptchaVo(specCaptcha.toBase64(), key);
}
条件注解
条件注解
@ConditionalOnProperty
,如下,该注解表达的含义是只有当minio.endpoint
属性存在时,该配置类才会生效。
#minio配置
minio:
endpoint: http://192.168.17.101:9000 # minio服务器地址
accessKey: minioadmin # 用户名
secretKey: minioadmin # 密钥
bucketName: lease #桶名称
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
@ConditionalOnProperty(name = "minio.endpoint")
public class MinioConfiguration {
@Autowired
private MinioProperties properties;
@Bean
public MinioClient minioClient() {
return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build();
}
}
短信验证码
置短信服务
创建AccessKey
云账号 AccessKey 是访问阿里云 API 的密钥,没有AccessKey无法调用短信服务。点击页面右上角的头像,选择AccessKey管理,然后创建AccessKey。
依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
</dependency>
配置
aliyun:
sms:
access-key-id: <access-key-id>
access-key-secret: <access-key-secret>
endpoint: dysmsapi.aliyuncs.com
上述access-key-id
、access-key-secret
需根据实际情况进行修改。
实体类
@Data
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliyunSMSProperties {
private String accessKeyId;
private String accessKeySecret;
private String endpoint;
}
将Client加入容器
@Configuration
@EnableConfigurationProperties(AliyunSMSProperties.class)
@ConditionalOnProperty(name = "aliyun.sms.endpoint")
public class AliyunSMSConfiguration {
@Autowired
private AliyunSMSProperties properties;
@Bean
public Client smsClient() {
Config config = new Config();
config.setAccessKeyId(properties.getAccessKeyId());
config.setAccessKeySecret(properties.getAccessKeySecret());
config.setEndpoint(properties.getEndpoint());
try {
return new Client(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Autowired
private Client client;
@Override
public void sendCode(String phone, String code) {
//phone是手机号,code是短信 ,用一个工具类生成一个6个随机0-9的数
SendSmsRequest smsRequest = new SendSmsRequest();
smsRequest.setPhoneNumbers(phone);
smsRequest.setSignName("阿里云短信测试");
smsRequest.setTemplateCode("SMS_154950909");
smsRequest.setTemplateParam("{\"code\":\"" + code + "\"}");
try {
client.sendSms(smsRequest); //发送短信
} catch (Exception e) {
throw new RuntimeException(e);
}
}
mybtis分页查询多表
Mybatis-Plus分页插件注意事项
使用Mybatis-Plus的分页插件进行分页查询时,如果结果需要使用
<collection>
进行映射,只能使用嵌套查询(Nested Select for Collection),而不能使用嵌套结果映射(Nested Results for Collection)。嵌套查询和嵌套结果映射是Collection映射的两种方式
嵌套结果映射
<select id="selectRoomPage" resultMap="RoomPageMap">
select ri.id room_id,
ri.number,
ri.rent,
gi.id graph_id,
gi.url,
gi.room_id
from room_info ri
left join graph_info gi on ri.id=gi.room_id
</select>
<resultMap id="RoomPageMap" type="RoomInfoVo" autoMapping="true">
<id column="room_id" property="id"/>
<collection property="graphInfoList" ofType="GraphInfo" autoMapping="true">
<id column="graph_id" property="id"/>
</collection>
</resultMap>
嵌套查询
<select id="selectRoomPage" resultMap="RoomPageMap">
select id,
number,
rent
from room_info
</select>
<resultMap id="RoomPageMap" type="RoomInfoVo" autoMapping="true">
<id column="id" property="id"/>
<collection property="graphInfoList" ofType="GraphInfo" select="selectGraphByRoomId" column="id"/>
</resultMap>
<select id="selectGraphByRoomId" resultType="GraphInfo">
select id,
url,
room_id
from graph_info
where room_id = #{id}
</select>
这种方法使用两个独立的查询语句来获取一对多关系的数据。首先,Mybatis会执行主查询来获取room_info
列表,然后对于每个room_info
,Mybatis都会执行一次子查询来获取其对应的graph_info
。
自定义RedisTemplate
缓存优化是一个性价比很高的优化手段,多数情况下,缓存优化可以通过一些简单的操作,换来性能的大幅提升。缓存优化的核心思想就是将一些原本保存在磁盘(例如MySQL)中的、经常访问并且查询开销比较大的数据,临时保存到内存(例如Redis)中。后序再访问相同数据时,就可直接从内存中获取结果,而无需再访问磁盘,由于内存的读写速度远高于磁盘,因此就能极大的提高程序的性能。
在使用缓存优化时,有一个问题不得不提,那就是数据库和缓存数据的一致性,当数据库中的数据发生变化时,缓存中的数据也要同步更新,否则就会出现数据不一致的问题,解决该问题的方案有如下几个
数据发生变化时,更新数据库的同时也更新缓存
数据发生变化时,更新数据库的同时删除缓存
在了解了缓存优化的核心思想后,我们以移动端中的
根据ID获取房间详情
接口为例,进行缓存优化。该接口涉及多表查询,查询时会多次访问数据库,查询代价较高,故可采取缓存优化,加快查询速度。
.自定义RedisTemplate
使用Reids保存缓存数据,因此我们需要使用RedisTemplate进行读写操作。前文提到过,
Spring-data-redis
提供了StringRedisTemplate
和RedisTemplate<Object,Object>
两个实例,但是两个实例均不满足我们当前的需求,所以我们需要自定义RedisTemplate。
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> stringObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.java());
return template;
}
}