尚庭公寓学习笔记

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类型中,包括文本、数字甚至是一个对象。

常用命令

setset命令用于添加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

decrdecr命令用于对数值做自减操作,具体语法如下。

DECR key

若key对应的value是整数,则返回自减后的结果,若不是整数则报错,若key不存在则创建并返回-1。

  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元素的命令有lindexlrange,各命令的功能与用法如下

lindex

该命令用于获取指定索引位置的元素,语法如下

lindex key index

lrange

该命令用于获取指定范围内的元素列表,语法如下

lrange key start stop

获取list全部元素,命令如下

lrange l1 0 -1

删除元素

删除list元素的命令有lpoprpoplrem,各命令的功能与用法如下

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表示的就是分数区间,该区间默认仍为闭区间。在该模式下,可以在startstop前增加(来表示开区间,例如(1 (5,表示的就是(1,5)这个开区间。除此之外,还可以使用-inf+inf表示负无穷和正无穷。

    • byscore:用于切换到分数模式

    • rev:表示降序排序。在byscore模式下使用rev参数需要注意查询区间,start应大于stop。

    • limit:该选项只用于byscore模式,作用和sql语句中的limit一致

    • withscores:用于打印分数

​​​​​
  1. 应用场景

    zset主要用于各种排行榜。

SpringBoot整合Redis

Spring Data Redis 是Spring大家族中的一个子项目,主要用于Spring程序和Redis的交互。它基于的Redis Java客户端(JedisLettuce)做了抽象,提供了一个统一的编程模型,使得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参数的类型不是StringWebDataBinder就会自动进行数据类型转换。SpringMVC提供了常用类型的转换器,例如StringIntegerStringDateStringBoolean等等,其中也包括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可以处理常用的数据类型转换,例如StringIntegerDate等等,其中也包含枚举类型,但是枚举类型的默认转换规则是枚举对象实例(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();
    }
}

 短信验证码

置短信服务

  • 开通短信服务

    • 阿里云官网,注册阿里云账号,并按照指引,完成实名认证(不认证,无法购买服务)

    • 找到短信服务,选择免费开通

    • 进入短信服务控制台,选择快速学习和测试

    • 找到发送测试下的API发送测试,绑定测试用的手机号(只有绑定的手机号码才能收到测试短信),然后配置短信签名和短信模版,这里选择[专用]测试签名/模版

创建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-idaccess-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提供了StringRedisTemplateRedisTemplate<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;
    }
}

 

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值