嘘~ 正在从服务器偷取页面 . . .

FreshCup 开发


1. JWT

1.1 跨域问题

  1. 用户向服务器发送用户名和密码;

  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等;

  3. 服务器向用户返回一个 session_id,写入用户的 Cookie;

  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器;

  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

以上是使用 session 进行的一般认证方式,但是对于集群服务器的跨域问题难以解决,这需要几个服务器共享 session。

解决方案:

  1. 配置 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败;
  2. 服务器将 session 保存在客户端,每次请求时发回服务器。比如 JWT。

1.2 结构样式

对于 JWT,拥有三个组成部分:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

1.3.1 Header

{
  "alg": "HS256",
  "typ": "JWT"
}
//alg 表示签名的算法(algorithm)
//typ 表示这个令牌的类型,JWT 令牌统一写为 JWT

1.3.2 Payload

JWT 规定了7个官方字段:

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

当然,可以在 Payload 中添加私有字段,包括 name、uid、role 等。

1.3.3 Signature

Signature 部分是对前两部分的签名,防止数据篡改。

需要指定一个密钥(secret),然后使用 Header 里指定的签名算法进行签名。

1.3 SpringBoot 结合 JWT

导入对应的 maven 依赖:

<!--导入 maven 依赖--> 
<dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>3.18.3</version>
</dependency>
        
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

SpringBoot 配置文件:

jwt:
  #jwt需要的密钥
  secret: 239FJAS993JASLVKCLS02JGFS
  #跟前端固定请求头
  prefix: Bearer_
  #jwt设置过期时间
  expiration: 864000

整合提取工具类:

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sast.jwt.common.enums.CustomError;
import com.sast.jwt.entity.Account;
import com.sast.jwt.exception.LocalRuntimeException;
import com.sast.jwt.mapper.AccountMapper;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

//jwt 实现工具类
@Component
@Data
public class JWTUtil {

    private final AccountMapper accountMapper;

    private final RedisUtil redisUtil;

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    @Value("${jwt.prefix}")
    private String prefix;

    public static final String USER_LOGIN_TOKEN = "TOKEN";

    public JWTUtil(RedisUtil redisUtil, AccountMapper accountMapper) {
        this.redisUtil = redisUtil;
        this.accountMapper = accountMapper;
    }

    /**
     * 给每个账户生成一个 token
     *
     * @param account 用户信息
     * @return token
     */
    public String generateToken(Account account) {
        Date nowDate = new Date();
        Date expiredDate = new Date(nowDate.getTime() + expiration * 1000);
        JWTCreator.Builder builder = JWT.create();
        builder.withClaim("id", account.getId());
        builder.withClaim("username", account.getUsername());
        builder.withClaim("role", account.getRole());
        builder.withExpiresAt(expiredDate);
        return prefix + builder.sign(Algorithm.HMAC256(secret));
    }

    //获取 token 具体信息
    public Map<String, Claim> getClaims(String token) {
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
            DecodedJWT decodedJWT = verifier.verify(token);
            return decodedJWT.getClaims();
        } catch (IllegalArgumentException | JWTVerificationException e) {
            throw new LocalRuntimeException(CustomError.TOKEN_ERROR);
        }
    }

    public Account getAccount(String token) {
        Map<String, Claim> map = getClaims(token);
        return new Account(
                map.get("id").asLong(),
                map.get("username").asString(),
                map.get("role").asInt());
    }

    //判断 token 是否过期
    /*public boolean isTokenExpired(String token) {
        Date expiresAt = getClaims(token).get("exp").asDate();
        return expiresAt.getTime() - System.currentTimeMillis() < 0;
    }*/

    //存储在 redis 中的 token 是否过期
    public boolean isTokenExpired(String token) {
        Account account = getAccount(token);
        long expiration = redisUtil.ddl(account.getUsername());
        return expiration <= 0;
    }

    //判断 token 是否需要刷新
    private boolean isTokenNeedRefresh(String key) {
        long expiration = redisUtil.ddl(key);
        return expiration < (this.expiration * 1000 >> 1);
    }

    //刷新 token 过期时间
    /*public String refreshToken(String token) {
        Date nowDate = new Date();
        Date expiredDate = new Date(nowDate.getTime() + expiration * 1000);
        Account account = getAccount(token);
        JWTCreator.Builder builder = JWT.create();
        builder.withClaim("id", account.getId());
        builder.withClaim("username", account.getUsername());
        builder.withClaim("role", account.getRole());
        builder.withExpiresAt(expiredDate);
        return prefix + builder.sign(Algorithm.HMAC256(secret));
    }*/

    //刷新 token 存在时间
    public void refreshExpiration(String key) {
        if (isTokenNeedRefresh(key)) {
            redisUtil.expire(key, expiration);
        }
    }

}

2. Redis

2.1 什么是 Redis

我们使用 Redis 来实现 NoSQL (not only sql)。

主要是作为一个中间件,在真实的服务中,有时像后端发送的信息量会很大,如果这期间还是使用数据库的话,读取速度会很慢。Redis 提供了一个缓存,这样写入和读取数据都要快上不少。

1.方便扩展(数据之间没有关系,很好扩展);

2.大数据高性能(Redis一秒写八万次,读取11万,NoSQL 缓存记录级);

3.数据类型是多样的;

4.传统 RDBMS 和 NoSQL。

Redis 是单线程的,但是运行速度十分的快,一秒钟读取接近十万条读取数据。

CPU>内存>硬盘。

核心:Redis 将所有数据全部放在内存中,所以读写速度非常快。

在之后的八股文整合中,会更加详细的描述 MySQL 以及 Redis 的内容。

2.2 启动 Redis

Windows 的 Redis 少有人去维护更新,现在的版本停留在5版本,所以还是建议 Linux Docker 部署 redis 服务。

Redis 的容器启动时,如果没有指定配置文件,会默认无配置文件启动(当然这本身是被允许的,Redis 本身就有很多被注释掉的配置)。

在 Redis 的配置文件中,在注释中有详细的解释。

本身的配置有着多达一千多行的完整说明

# 首先创建好一个本地的 Redis 配置文件
mkdir conf
cd conf
touch redis.conf
vim redis.conf

# 创建 data 目录,用于存储持久化缓存的 redis 数据
mkdir data

# 配置文件内容:
protected-mode no
requirepass sast_forever
# 我们只需要关闭保护模式和配置密码就可以直接使用了
# 具体配置信息可以参考 Windows 端的 redis 的配置文件说明

# 指定本地的 redis.conf 文件和容器的配置文件绑定
docker run -p 7000:6379 --name redis -v $PWD/conf/redis.conf:/etc/redis/redis.conf -v $PWD/data:/redis/data -d redis redis-server /etc/redis/redis.conf

# 可以直接在启动的时候直接使用参数配置连接密码(不推荐)
--requirepass "sast_forever"
redis-server /etc/redis/redis.conf:使用指定的配置文件启动redis

可以使用 Redis DeskTop Manager 对 Redis 的连接管理(可以像 navicat 那样进行桥接连接)。

2.3 SpringBoot 整合 redis

jedis 使用 Java 来操作 Redis。(在 springboot2.x 之后已经被改成 lettuce)

jedis:采用的直连,多个线程操作话是不安全的,如果想要避免;

lettuce:采用netty(高性能网络结构,异步传值),实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据。

原子性(atomicity):

  • 一个事务是一个不可分割的最小工作单位,事务中包括的诸操作要么都做,要么都不做;

  • Redis 所有单个命令的执行都是原子性的,这与它的单线程机制有关;

  • Redis 命令的原子性使得我们不用考虑并发问题,可以方便的利用原子性自增操作INCR实现简单计数器功能。

package com.sast.atsast;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @program: atSast
 * @summary: 对 jedis 进行测试
 * @author: cxy621
 * @create: 2021-07-23 13:45
 **/
@SpringBootTest
public class JedisTest {

    @Autowired
    private RedisTemplate redisTemplate;
	//opsforValue String
    //opsforList List
    //opsforSet Set
    //opsforHash Hash
    //opsforZSet Zset
    @Test
    public void lettuceTest() {
//        RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
//        redisConnection.flushAll();
//        redisConnection.flushDb();

        redisTemplate.opsForValue().set("name", "cxy");
        System.out.println(redisTemplate.opsForValue().get("name"));

    }

}

所有的对象需要序列化如果没有序列化就会报错。对于没有序列化的值,Java 中会有自带的 jdk 自带的序列化,但是如果不自己配置redis,会自动出现转义字符。可以自己自己去设置 redis 工具类,重写 redisUtil,就不需要调用原生麻烦的包了。

配置文件中的配置:

spring:
  redis:
    # Redis 数据库索引(默认为0)
    database: 0
    host: localhost
    port: 6379	
    timeout: 180000
    jedis:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0

通过源码可以看出,SpringBoot 自动帮我们在容器中生成了一个 RedisTemplate 和一个 StringRedisTemplate。但是,这个RedisTemplate 的泛型是 <Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为 <String,Object> 形式的 RedisTemplate。并且,这个RedisTemplate 没有设置数据存在 Redis 时,key 及 value 的序列化方式。

编写 redis 配置类,内容如下,在该类中完成 Jedis 池、Redis 连接和 RedisTemplate 序列化三个配置完成 SpringBoot 整合 redis 的进一步配置。其中 RedisTemplate 对 key 和 value 的序列化类,各人结合自己项目情况进行选择即可。

配置 redis config 文件,防止出现转义字符的问题:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration
                .defaultCacheConfig().entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext
                        .SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();
        return cacheManager;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        // key序列化方式
        template.setKeySerializer(redisSerializer);
        // value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }
    
}

除此之外,直接这样使用 RedisTemplate 还是会比较麻烦,我们可以自己写一个工具类:

import com.sast.jwt.common.enums.CustomError;
import com.sast.jwt.exception.LocalRuntimeException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private final StringRedisTemplate redisTemplate;

    public RedisUtil(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public void set(String key, String value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);//TimeUnit.SECONDS 设置时间单位为秒
    }
    
    public void set(String key, String value, long time, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, time, unit);
    }

    // 设置过期时间
    public void expire(String key, long time) {
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }
    
    public void expire(String key, long time, TimeUnit unit) {
        redisTemplate.expire(key, time, unit);
    }

    // 获取过期截止时间
    public long ddl(String key) {
        if (hasKey(key)) {
            return redisTemplate.getExpire(key);
        }
        throw new LocalRuntimeException(CustomError.TOKEN_ERROR);
    }

    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public boolean hasKey(String key) {
        String token = get(key);
        return token != null;
    }
    
    /**
     * 删除缓存
     *
     * @param key 可以传一个值或多个
     * 使用 ... 表示不确定
     */
    @SuppressWarnings("unchecked")
    // 忽略编译器中的警告错误,比如 unused
    public void del(String... key) {
        if (key != null && key.length > 0) {
            for (String s : key) {
                redisTemplate.delete(s);
            }
        }
    }
    
}

2.4 整合 JWT 和 Redis

yaml 自定义配置 jwt 参数:

jwt:
  # jwt需要的密钥
  secret: 239FJAS993JASLVKCLS02JGFS
  # 跟前端固定请求头
  header: Token
  # jwt设置过期时间
  expiration: 864000

配置 jwt 工具类:

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sast.jwt.common.enums.CustomError;
import com.sast.jwt.entity.Account;
import com.sast.jwt.exception.LocalRuntimeException;
import com.sast.jwt.mapper.AccountMapper;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

// jwt 实现工具类
@Component
@Data
public class JwtUtil {

    private final AccountMapper accountMapper;

    private final RedisUtil redisUtil;

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    @Value("${jwt.prefix}")
    private String prefix;

    public static final String USER_LOGIN_TOKEN = "TOKEN";

    public JwtUtil(RedisUtil redisUtil, AccountMapper accountMapper) {
        this.redisUtil = redisUtil;
        this.accountMapper = accountMapper;
    }

    // 给每个账户生成一个 token
    public String generateToken(Account account) {
        Date nowDate = new Date();
        Date expiredDate = new Date(nowDate.getTime() + expiration * 1000);
        JWTCreator.Builder builder = JWT.create();
        builder.withClaim("id", account.getId());
        builder.withClaim("username", account.getUsername());
        builder.withClaim("role", account.getRole());
        builder.withExpiresAt(expiredDate);
        return prefix + builder.sign(Algorithm.HMAC256(secret));
    }

    // 获取 token 具体信息
    public Map<String, Claim> getClaims(String token) {
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
            DecodedJWT decodedJWT = verifier.verify(token);
            return decodedJWT.getClaims();
        } catch (IllegalArgumentException | JWTVerificationException e) {
            throw new LocalRuntimeException(CustomError.TOKEN_ERROR);
        }
    }

    public Account getAccount(String token) {
        Map<String, Claim> map = getClaims(token);
        return new Account(
                map.get("id").asLong(),
                map.get("username").asString(),
                map.get("role").asInt());
    }

    // 判断 token 是否过期
    /*public boolean isTokenExpired(String token) {
        Date expiresAt = getClaims(token).get("exp").asDate();
        return expiresAt.getTime() - System.currentTimeMillis() < 0;
    }*/

    // 存储在 redis 中的 token 是否过期
    public boolean isTokenExpired(String token) {
        Account account = getAccount(token);
        long expiration = redisUtil.ddl(account.getUsername());
        return expiration <= 0;
    }

    // 判断 token 是否需要刷新
    private boolean isTokenNeedRefresh(String key) {
        long expiration = redisUtil.ddl(key);
        return expiration < (this.expiration * 1000 >> 1);
    }

    // 刷新 token 过期时间
    /*public String refreshToken(String token) {
        Date nowDate = new Date();
        Date expiredDate = new Date(nowDate.getTime() + expiration * 1000);
        Account account = getAccount(token);
        JWTCreator.Builder builder = JWT.create();
        builder.withClaim("id", account.getId());
        builder.withClaim("username", account.getUsername());
        builder.withClaim("role", account.getRole());
        builder.withExpiresAt(expiredDate);
        return prefix + builder.sign(Algorithm.HMAC256(secret));
    }*/

    // 刷新 token 存在时间
    public void refreshExpiration(String key) {
        if (isTokenNeedRefresh(key)) {
            redisUtil.expire(key, expiration);
        }
    }

}

上面其实是一定程度有悖 JWT 的设计初衷,因为 JWT 的本意是将 token 存放在客户端,但其实我们将 token 存放在服务端。但是这也是为了安全性的考虑,因为我们需要验证是否是用户本人。

并且,由于 token 不像 cookie,不可以进行手动过期,对于过期 token 必须重新生成一个新的 token。在 FC 中,将 token 存入 redis,采取 redis 的过期时间来对 token 进行反复刷新操作。

在和一些已经工作的学长的交流中,对于登录的验证来说,不建议使用 token。在权限管理中,可能更加具有普适性。并且 token 和 session 并非是完全的替代关系,对于一些不规范的程序开发来说,

为了维护 JWT 的安全性,需要有一系列的考虑,之后可能会说。

2.5 Redis 数据结构

2.5.1 String

String 的实现类似于 Java 中的 ArrayList,作为变长字符串。

一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

常用命令:set, get, strlen, exists, decr, incr, setex, mset, mget

String 数据结构图示

2.5.2 List

实现类似 Java 中的 LinkedList,双向链表,对两端操作效率较高,但不适合使用索引查找。

List 的数据结构为快速链表 quickList。在列表元素较少的情况下会使用一块连续的内存存储 ziplist,分配连续的内存。当ziplist节点个数过多,quicklist 退化为双向链表,一个极端的情况就是每个 ziplist 节点只包含一个 entry,即只有一个元素。当 ziplist 元素个数过少时,quicklist 可退化为 ziplist,一种极端的情况就是 quicklist 中只有一个 ziplist 节点。

List 数据结构图示

常用命令:rpush, lpop, lpush, rpop, lrange, llen

2.5.3 Hash

hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,可以只修改值中某个字段。

常用命令:hset, hmset, hexists, hget, hgetall, hkeys, hvals

hmset 1 name "cxy621" age 18 birthday 8-28

hgetall 1
# 1) "name"
# 2) "cxy621"
# 3) "age"
# 4) "18"
# 5) "birthday"
# 6) "8-28"

Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。

2.5.4 Set

set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序,数据不重复。

常用命令:sadd, spop, smembers, sismember, scard, sinterstore, sunion

127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
(integer) 2
127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
(integer) 0
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
1) "value1"
2) "value2"
127.0.0.1:6379> scard mySet # 查看 set 的长度
(integer) 2
127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素
(integer) 1
127.0.0.1:6379> sadd mySet2 value2 value3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "value2"

2.5.5 ZSet

sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。

常用命令:zadd, zcard, zscore, zrange, zrevrange, zrem

Redis 采用的是跳跃表,跳跃表效率堪比红黑树,实现远比红黑树简单。

查询数据“51”

2.5.6 新的类型

  1. geospatial:Redis 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离

  2. hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV

  3. bitmap:bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等

3. AOP

3.1 定义

面向对象编程(OOP)的好处是显而易见的,缺点也同样明显。当需要为多个不具有继承关系的对象添加一个公共的方法的时候,例如日志记录、性能监控等,如果采用面向对象编程的方法,需要在每个对象里面都添加相同的方法,这样就产生了较大的重复工作量和大量的重复代码,不利于维护。面向切面编程(AOP)是面向对象编程的补充,简单来说就是统一处理某一“切面”的问题的编程思想。

AOP 结构图片

与面向对象的顺序流程不同,AOP采用的是横向切面的方式,注入与主业务流程无关的功能,例如事务管理和日志管理。如果使用AOP的方式进行日志的记录和处理,所有的日志代码都集中于一处,不需要再每个方法里面都去添加,极大减少了重复代码。

3.2 AOP 专业术语

通知(Advice)包含了需要用于多个应用对象的横切行为,完全听不懂,没关系,通俗一点说就是定义了“什么时候”和“做什么”;

连接点(Join Point)是程序执行过程中能够应用通知的所有点;

切点(Poincut)是定义了在“什么地方”进行切入,哪些连接点会得到通知。显然,切点一定是连接点;

切面(Aspect)是通知和切点的结合。通知和切点共同定义了切面的全部内容——是什么,何时,何地完成功能;

引入(Introduction)允许我们向现有的类中添加新方法或者属性;

织入(Weaving)是把切面应用到目标对象并创建新的代理对象的过程,分为编译期织入、类加载期织入和运行期织入。

示例图片

3.3 Spring-Boot 中使用 AOP

3.3.1 定义切面和切点

Spring 采用@Aspect注解对 POJO 进行标注,该注解表明该类不仅仅是一个 POJO,还是一个切面。切面是切点和通知的结合,那么定义一个切面就需要编写切点和通知。在代码中,只需要添加 @Aspect 注解即可。

切点是通过@Pointcut注解和切点表达式定义的。

@Pointcut注解可以在一个切面内定义可重用的切点。

由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且实际中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。如图是execution表达式的语法:

execution表示在方法执行的时候触发。以*开头,表明方法返回值类型为任意类型。然后是全限定的类名和方法名,* 可以表示任意类和任意方法。对于方法参数列表,可以使用“..”表示参数为任意类型。如果需要多个表达式,可以使用“&&”、“||”和“!”完成与、或、非的操作。

execution( 方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数) )  
  "*"表示不限     ".."表示参数不限
  方法修饰符不写表示不限,不用"*" 

对指定路径的方法操作

3.3.2 使用通知

通知有五种类型,分别是:

前置通知(@Before):在目标方法调用之前调用通知;

后置通知(@After):在目标方法完成之后调用通知;

环绕通知(@Around):在被通知的方法调用之前和调用之后执行自定义的方法;

返回通知(@AfterReturning):在目标方法成功执行之后调用通知;

异常通知(@AfterThrowing):在目标方法抛出异常之后调用通知。

3.3.3 简单例子

首先是简单写几个控制器,方便我们之后随机进行测试。

写了一个带传递的参数是为了方面之后的切面中的演示,下面对日志功能进行简单演示:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/hello")
    public String sayHello() {
        System.out.println("Hello, World");
        return "Hello";
    }

    @GetMapping("/hello/{name}")
    public String sayLove(@PathVariable("name") String name) {
        System.out.println("Activate Successfully");
        return "i love you " + name;
    }
}

首先定义切入点,切入点的位置定好之后之后便可使用注释将对应的通知插入。

这里使用 @Slf4j 的注释,使用日志(这里使用夏佬的例子进行简单的说明)。

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@Aspect
public class AopAdvice {
    //表示实体类中所有的方法
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void MyPointCut() {
    }

    @Before("MyPointCut()")
    public void BeforeAdvice() {
        log.info("this is before");
    }

    @After("MyPointCut()")
    public void AfterAdvice() {
        log.info("this is after");
    }

    @Around("MyPointCut()")
    public Object AroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String methodName = proceedingJoinPoint.getSignature().getName();
        String className = proceedingJoinPoint.getTarget().getClass().toString();
        ObjectMapper objectMapper = new ObjectMapper();
        Object[] array = proceedingJoinPoint.getArgs();//获取其中的参数
        log.info("调用前:" + className + ":" + methodName + " args=" + objectMapper.writeValueAsString(array));
        Object object = proceedingJoinPoint.proceed();//对应事件开始处理,这里需要抛出异常
        log.info("调用后:" + className + ":" + methodName + " args=" + objectMapper.writeValueAsString(array));
        return object;
    }
}

调用 /hello

调用 /hello/cxy

3.4 AOP 结合 JWT

创建自定义注释:

import com.sast.jwt.enums.AuthEnum;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//创建一个自定义注释,里面的内容规定为需要的权限
public @interface AuthHandler {

    AuthEnum value();

}

创建一个 AOP 的切面,绑定到注释:

import com.sast.jwt.annotation.AuthHandler;
import com.sast.jwt.entity.Account;
import com.sast.jwt.enums.AuthEnum;
import com.sast.jwt.enums.CustomError;
import com.sast.jwt.exception.LocalRuntimeException;
import com.sast.jwt.interceptor.LoginInterceptor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AuthAspect {
	
    //将切点设置为注释,只要触发注释便会触发对应的方法
    @Pointcut("@annotation(com.sast.jwt.annotation.AuthHandler)")
    public void start() {

    }

    //绑定切入点和切入点形参
    @Before("start()&&@annotation(authHandler)")
    public Object authJudge(JoinPoint joinPoint, AuthHandler authHandler) {
        AuthEnum authEnum = authHandler.value();
        Account account = LoginInterceptor.accountThreadLocal.get();
        if (!AuthEnum.checkAuth(account, authEnum)) {
            throw new LocalRuntimeException(CustomError.AUTHENTICATION_ERROR);
        }
        return joinPoint;
    }
}

配置拦截器,对发送的 Token 进行判断:

import com.sast.jwt.dao.AccountDao;
import com.sast.jwt.entity.Account;
import com.sast.jwt.enums.CustomError;
import com.sast.jwt.exception.LocalRuntimeException;
import com.sast.jwt.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    //在多个线程中传递 token
    public static ThreadLocal<Account> accountThreadLocal = new ThreadLocal<>();

    private final AccountDao accountDao;

    private final JwtUtil jwtUtil;

    public LoginInterceptor(JwtUtil jwtUtil, AccountDao accountDao) {
        this.jwtUtil = jwtUtil;
        this.accountDao = accountDao;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler)
            throws Exception {
        //规定发送的头中带有 Token 字段
        String header = request.getHeader("Token");
        //规定 Token 中的开头为 Bearer_,以便以后的功能扩展
        if (!StringUtils.hasLength(header) || !header.startsWith("Bearer_")) {
            throw new LocalRuntimeException(CustomError.TOKEN_ERROR);
        }
        String token = header.substring(7);
        log.info(token);
        Account account = jwtUtil.getAccount(token);
        log.info(String.valueOf(account));
        if (account != null) {
            if (jwtUtil.isTokenExpired(token)) {
                throw new LocalRuntimeException(CustomError.TOKEN_OUT_TIME);
            }
            if (accountDao.selectById(account.getId()) != null) {
                jwtUtil.refreshExpiration(account.getUsername());
                accountThreadLocal.set(account);
                return true;
            }
        }
        throw new LocalRuntimeException(CustomError.TOKEN_ERROR);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex)
            throws Exception {
        accountThreadLocal.remove();
    }
    
}

配置拦截器拦截的 url:

import com.sast.jwt.converter.CustomJsonHttpMessageConverter;
import com.sast.jwt.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Configuration
@Component
public class WebMvcConfig extends WebMvcConfigurationSupport {

    private final CustomJsonHttpMessageConverter converter;

    private final LoginInterceptor loginInterceptor;

    public WebMvcConfig(CustomJsonHttpMessageConverter converter,
                        LoginInterceptor loginInterceptor) {
        this.converter = converter;
        this.loginInterceptor = loginInterceptor;
    }

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //这里需要使用自动装配的拦截器,需要公用 ThreadLocal
        //所以这里不能每次都创建新的拦截器,而是需要使用 IOC 帮我们feng'z
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login", "/user/register");
    }
    
}

3.5 ThreadLocal 使用

对于 Java 中的多线程中,我们需要传递同一个参数,这个时候就可以使用 ThreadLocal,对每个一线程共享这个对象。

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
//一般写法

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

不过要在最后注意使用threadLocalUser.remove(); 将线程清除。

4. 验证码

需要添加对应依赖包:

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

设计生成图片验证码的接口,将生成的验证码存放在 redis 中。

通过 UUID 给二维码生成独一无二的 uid。

UUID由以下几部分的组合:

  1. 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同;

  2. 时钟序列;

  3. 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

配置图片验证码:

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        // 图片边框
        properties.setProperty("kaptcha.border", "no");
        // 边框颜色
        properties.setProperty("kaptcha.border.color", "black");
        //边框厚度
        properties.setProperty("kaptcha.border.thickness", "1");
        // 图片宽
        properties.setProperty("kaptcha.image.width", "200");
        // 图片高
        properties.setProperty("kaptcha.image.height", "50");
        //图片实现类
        properties.setProperty("kaptcha.producer.impl", "com.google.code.kaptcha.impl.DefaultKaptcha");
        //文本实现类
        properties.setProperty("kaptcha.textproducer.impl", "com.google.code.kaptcha.text.impl.DefaultTextCreator");
        //文本集合,验证码值从此集合中获取
        properties.setProperty("kaptcha.textproducer.char.string", "01234567890");
        //验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字体
        properties.setProperty("kaptcha.textproducer.font.names", "宋体");
        //字体颜色
        properties.setProperty("kaptcha.textproducer.font.color", "black");
        //文字间隔
        properties.setProperty("kaptcha.textproducer.char.space", "5");
        //干扰实现类
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
        //干扰颜色
        properties.setProperty("kaptcha.noise.color", "blue");
        //干扰图片样式
        properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
        //背景实现类
        properties.setProperty("kaptcha.background.impl", "com.google.code.kaptcha.impl.DefaultBackground");
        //背景颜色渐变,结束颜色
        properties.setProperty("kaptcha.background.clear.to", "white");
        //文字渲染器
        properties.setProperty("kaptcha.word.impl", "com.google.code.kaptcha.text.impl.DefaultWordRenderer");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

5. 完整登录逻辑

创建工具类,获取存储在 redis 中的 token:

import com.sast.jwt.entity.Account;

public class RedisKeyFetch {

    public static String getTokenKey(Account account){
        return "TOKEN:" + account.getUsername();
    }

}

使用 LoginController:

import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.sast.jwt.common.contents.RedisKeyFetch;
import com.sast.jwt.entity.Account;
import com.sast.jwt.exception.LocalRuntimeException;
import com.sast.jwt.mapper.AccountMapper;
import com.sast.jwt.utils.JWTUtil;
import com.sast.jwt.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@RestController
public class LoginController {

    public static final String LOGIN_VALIDATE_CODE = "VAL_CODE:";

    private final DefaultKaptcha kaptchaProducer;

    private final RedisUtil redisUtil;

    private final AccountMapper accountMapper;

    private final JWTUtil jwtUtil;

    public LoginController(DefaultKaptcha kaptchaProducer, RedisUtil redisUtil, AccountMapper accountMapper, JWTUtil jwtUtil) {
        this.kaptchaProducer = kaptchaProducer;
        this.redisUtil = redisUtil;
        this.accountMapper = accountMapper;
        this.jwtUtil = jwtUtil;
    }

    /**
     * 返回验证码图片,并将验证码存入Redis
     *
     * @param response 设置响应头参数信息
     */
    @GetMapping("/getValidateCode")
    public void getImgValidateCode(HttpServletResponse response) {
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");

        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        //为每个请求生成一个唯一的验证码
        response.addHeader("CAPTCHA", uuid);
        response.setContentType("image/jpeg");

        String capText = kaptchaProducer.createText();
        BufferedImage bi = kaptchaProducer.createImage(capText);

        try {
            ServletOutputStream out = response.getOutputStream();
            ImageIO.write(bi, "jpg", out);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }

        redisUtil.set(LOGIN_VALIDATE_CODE + uuid, capText, 60 * 5);
    }


    @PostMapping("/login")
    public Map<String, String> login(@RequestBody Account account,
                                     @RequestHeader("User-Agent") String agent,
                                     @RequestParam("validateCode") String validateCode,
                                     @RequestHeader("CAPTCHA") String uuid) {
        //验证验证码
        String currentCode = redisUtil.get(LOGIN_VALIDATE_CODE + uuid);
        if (currentCode == null) {
            throw new LocalRuntimeException("验证码失效");
        } else if (!currentCode.equals(validateCode)) {
            throw new LocalRuntimeException("验证码错误");
        }
        redisUtil.del(LOGIN_VALIDATE_CODE + uuid);

        //登录处理
        QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", account.getUsername());
        Account accountFromDB = accountMapper.selectOne(queryWrapper);
        if (accountFromDB == null) {
            throw new LocalRuntimeException("账号不存在");
        } else if (!SecureUtil.md5(account.getPassword()).equals(accountFromDB.getPassword())) {
            throw new LocalRuntimeException("密码错误");
        }
        String token = jwtUtil.generateToken(accountFromDB);
        Map<String, String> map = new HashMap<>();
        map.put("role", accountFromDB.getRole().toString());
        map.put("token", token);

        log.info("===============================================");
        log.info("用户登录:{},role:{}", accountFromDB.getUsername(), accountFromDB.getRole());
        log.info("登录Agent:{}", agent);
        log.info("===============================================");

        //用 Redis 中的过期时间代替 JWT 的过期时间,每次经过拦截器时更新过期时间
        //先设置为30天
        redisUtil.set(RedisKeyFetch.getTokenKey(account), token, 30, TimeUnit.DAYS);
        return map;
    }

}

6. open api

6.1 swagger

配置 swagger 配置文件:

import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
@EnableOpenApi//开启 Swagger 功能
public class SwaggerConfig {

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo()).enable(true)
                .select()
                //apis: 添加swagger接口提取范围
                .apis(RequestHandlerSelectors.basePackage("com.sast.jwt.controller"))
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("toy_Backend 项目接口文档")
                .description("toy_Backend 项目描述")
                .contact(new Contact("cxy621", "https://hexo.cxy621.top", "1580779474@qq.com"))
                .version("1.0")
                .build();
    }

}

但在 SpringBoot 更新到新版本之后,springfox 已经不再更新了,所以之后 swagger 的使用频率一定会减少,甚至停用。

在 SpringBoot 2.6.X 版本及以上使用 swagger 时,会出现Failed to start bean 'documentationPluginsBootstrapper';的错误。

因为 Springfox 使用的路径匹配是基于 AntPathMatche r的,而 SpringBoot 2.6.X 使用的是 PathPatternMatcher。

我们需要修改配置文件:

spring.mvc.pathmatch.matching-strategy: ANT_PATH_MATCHER

注意:对于 swagger 来说,返回的页面会被我们的全局异常给捕获,所以无法正常显示。

好消息是,我们有一个新的 open api 生成工具。

6.2 springdoc

依赖导入:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.7</version>
</dependency>

springdoc 相比于单纯的 swagger,不需要配置 config 文件就能够直接使用。

默认 json 数据访问链接🔗为:/v3/api-docs/

默认 swagger 页面访问链接🔗为:swagger-ui.html

可以在对应的 yaml 文件中配置制定自定义路径:

springdoc:
  swagger-ui:
    path: /swagger-ui-custom.html
  api-docs:
    path: /api-docs

之后打开对应链接,就可以打开 open-api 界面。

之后,需要在对应 controller 中加入 swagger 注释,让生成 api 具有一定的可读性。

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
//使用 @Tag 对 controller 进行确认
@Tag(name = "测试接口", description = "第一次使用 swagger 形式进行书写")
public class TestController {
    @GetMapping("/hello")
    @Operation(summary = "输出 hello world")
    public String index() {
        return "Hello World!";
    }

    @PostMapping("")
    public String test() {
        return "Hello!";
    }

    @GetMapping("/call/{name}")
    @Operation(summary = "对特定的用户问好", description = "需要传入对应的用户名",
               //对需要的参数进行声明,会通过注释来判断参数的类型
            parameters = {
                    @Parameter(name = "name", description = "用户名", required = true)
            })
    public String call(@PathVariable String name) {
        return "Hello " + name + "!";
    }
}

7. poi

依赖包导入:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.17</version>
</dependency>

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.15</version>
</dependency>

FreshCup 过程中的示例:

public Workbook importAdmin(Long contestId, MultipartFile file)
    throws IOException {
    if (contestMapper.selectOne(new LambdaQueryWrapper<Contest>()
                                .eq(Contest::getId, contestId)) == null) {
        throw new LocalRunTimeException(ErrorEnum.NO_CONTEST);
    }
	
    AtomicInteger success = new AtomicInteger(0);
    AtomicInteger failure = new AtomicInteger(0);

    // 创建一个新的工作簿
    Workbook output = new XSSFWorkbook();
    // 创建一个“结果” sheet
    Sheet sheetOutput = output.createSheet("结果");
    Row rowOutput = sheetOutput.createRow(0);
    rowOutput.createCell(0).setCellValue("账号");
    rowOutput.createCell(1).setCellValue("密码");

    //通过文件流,读取文件的文件流
    Workbook workbook = new XSSFWorkbook(file.getInputStream());
    Sheet sheet = workbook.getSheetAt(0);
    int rowNum = sheet.getLastRowNum();
    for (int i = 1; i <= rowNum; i++) {
        Row row = sheet.getRow(i);
        Cell cell = row.getCell(0);
        String username = cell.getStringCellValue();
        Account account = accountMapper.selectOne(new LambdaQueryWrapper<Account>()
                                                  .eq(Account::getUsername, username));
        if (account != null) {
            if (!account.getRole().equals(1)) {
                throw new LocalRunTimeException(ErrorEnum.ROLE_ERROR);
            }
            log.info(username + " 管理员已导入,无需再次导入");
            failure.getAndIncrement();
        } else {
            log.info(username + " 管理员导入成功");
            success.getAndIncrement();

            rowOutput = sheetOutput.createRow(success.get());
            rowOutput.createCell(0).setCellValue(username);
            String password = createAdmin(username);
            rowOutput.createCell(1).setCellValue(password);
        }
        addContestUser(contestId, username, accountMapper, accountContestManagerMapper);
    }
    //判断是否有创建成功的学生账号
    if (sheetOutput.getLastRowNum() == 0) {
        sheetOutput.createRow(1);
    }
    sheetOutput.getRow(0).createCell(3).setCellValue("成功导入" + success.get() + "个");
    sheetOutput.getRow(1).createCell(3).setCellValue("失败" + failure.get() + "个");
    return output;
}

参考文章

  1. JSON Web Token 入门教程
  2. AOP 获取方法上的注解,并修改注解内容
  3. SpringBoot 整合 Redis 以及工具类撰写
  4. 使用 ThreadLocal
  5. SpringBoot 整合 Captcha 验证码
  6. Java 生成 UUID
  7. 升级 SpringBoot 2.6.x 版本后,Swagger 没法用了
  8. SpringBoot 集成 swagger3
  9. SpringDoc 生成 OpenAPI3.0
  10. SpringBoot2 集成 springdoc-openapi-ui
  11. SpringBoot 中 poi 操作合集
  12. Redis 数据结构快速链表
  13. Redis 学习记录

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录