# java中常见方法总结

返回:Java开发

🐉 trycatch的高阶用法学习 🐉 replace的高阶用法学习
JSON 分布式唯一ID算法
if_else太多,如何重构(策略模式) 单点登录CAS
java8时间的序列化与反序列化 经典JAVA算法题册
获取 /resources 目录资源文件的 9 种方法

# classForName

返回:java中常见方法总结

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
1
2
3
4
5
6
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
                                ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@param initialize if {@code true} the class will be initialized.

根据运行结果得出Class.forName加载类时将类进了初始化,而ClassLoader的loadClass并没有对类进行初始化,只是把类加载到了虚拟机中。

# 保留有效数字

返回:java中常见方法总结

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;

public class TestDemo{
    public static void main(String[] args) {
        double num1 = 100.13145;

        //保留4位小数 100.1315 四舍五入
        BigDecimal bigDecimal = new BigDecimal(num1);
        double num2 = bigDecimal.setScale(4, BigDecimal.ROUND_HALF_UP).doubleValue();
        System.out.println(num2);

        // 保留4位小数 100.1315 四舍五入
        DecimalFormat decimalFormat = new DecimalFormat("#.0000");
        String stringValue = decimalFormat.format(num1);
        System.out.println(stringValue);
        Double doubleValue = Double.parseDouble(decimalFormat.format(num1));
        System.out.println(doubleValue);

        // 保留4位小数 100.1315 四舍五入
        System.out.println(String.format("%.4f", num1));

        // 保留4位小数 100.1315 四舍五入
        Double get_double = (double) ((Math.round(num1 * 10000)) / 10000.0);
        System.out.println(get_double);

        // 保留4位小数 100.1315
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        numberFormat.setMaximumFractionDigits(4);
        System.out.println(numberFormat.format(num1));


        // 保留4位小数 100.1314
        float num3 = (float) 100.13145;
        float a = (float) (Math.round(num3 * 10000)) / 10000;
        System.out.println(a);

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 进一法

List<String> lists = generateList();
int listSize = lists.size();
int baseInt = 800;
int loopTimes = (int) Math.ceil((double) listSize / baseInt);
1
2
3
4

# 二进制操作

返回:java中常见方法总结

# 判断2的倍数

boolean powerOfTwo(int n){
    if (n <= 0){
        return false;
    }
    return (n & (n-1)) == 0;
}
1
2
3
4
5
6

2的倍数二进制的形式一定是1000,2的倍数-1的二进制的形式一定是111,所以与操作一定是0

# 判断字符串空

返回:java中常见方法总结

# 针对空格做处理,排除空格、制表等空白符号

com.mysql.jdbc. StringUtils. isEmptyOrWhitespaceOnly
public static boolean isEmptyOrWhitespaceOnly(String str) {
        if (str == null || str.length() == 0) {
            return true;
        }
        int length = str.length();
        for (int i = 0; i < length; i++) {
            if (!Character.isWhitespace(str.charAt(i))) {
                return false;
            }
        }

        return true;
    }
//(java.lang.Character)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 不排除空白字符

public static boolean isEmpty(String str) {
        if (str == null || str.length() == 0) {
            return true;
        }
        return false;
    }

public static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }
1
2
3
4
5
6
7
8
9
10

# java字符串中反斜杠的问题

返回:java中常见方法总结

到斜杠\

regex中"\\"表示一个"\",在java中一个"\"也要用"\\"表示。这样,前一个"\\"代表regex中的"\",后一个"\\"代表java中的"\"
str.replaceAll("\\\\","");

# 0、’0’、\0

返回:java中常见方法总结

字符'0':char c = '0'; 它的ASCII码实际上是48。内存中存放表示:00110000
字符'\0' : ASCII码为0,表示一个字符串结束的标志。这是转义字符。
整数0 :ASCII码为0,字符表示为空字符,NULL;数值表示为0;内存中表示为:00000000

# main方法相关集锦

返回:java中常见方法总结

# 防止主线程退出

System.in.read();
1

# 计算地球两点距离

返回:java中常见方法总结

# 数学计算

public class DistanceCalUtil {
    /**
     - 地球半径
     */
    private static final Double EARTH_RADIUS = 6378.137;
    private static final String EMPTY_STRING = "";

    /**
     - 计算两点距离,单位:米
     - @param long1 经度1,objects[0]
     - @param lat1 纬度1,objects[1]
     - @param long2 经度2,objects[2]
     - @param lat2 纬度2,objects[3]
     - @return double
     */
    public static String getDistance(Object[] objects){
        for (Object obj : objects){
            if (StringUtil.isEmptyObj(obj)){
                return EMPTY_STRING;
            }
        }
        double radLat1 = rad(Double.valueOf(ObjectUtils.getDisplayString(objects[1])));
        double radLat2 = rad(Double.valueOf(ObjectUtils.getDisplayString(objects[3])));
        double latDiff = radLat1 - radLat2;
        double longDiff = rad(Double.valueOf(ObjectUtils.getDisplayString(objects[0])))
                -rad(Double.valueOf(ObjectUtils.getDisplayString(objects[2])));
        double dis = 2 - Math.asin(Math.sqrt(Math.pow(Math.sin(latDiff / 2), 2)
                + Math.cos(radLat1) - Math.cos(radLat2)
                - Math.pow(Math.sin(longDiff / 2), 2)));
        dis = dis - EARTH_RADIUS;
        dis = Math.round(dis*10000d)/10000d;
        dis = dis - 1000;
        return String.valueOf(dis);
    }

    /**
     -  计算弧度
     - @param num
     - @return
     */
    private static double rad(double num){
        return num - Math.PI/180.0;
    }

    public static void main(String... args){
        Double[] doubles = new Double[]{118.24255,24.33,118.24254,24.33};
        System.out.println(getDistance(doubles));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 引入jar包

<!-- https://mvnrepository.com/artifact/org.gavaghan/geodesy -->
        <dependency>
            <groupId>org.gavaghan</groupId>
            <artifactId>geodesy</artifactId>
            <version>1.1.3</version>
        </dependency>
1
2
3
4
5
6

计算距离

package com.chlm.mysession.common;

import lombok.extern.slf4j.Slf4j;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GeodeticCalculator;
import org.gavaghan.geodesy.GeodeticCurve;
import org.gavaghan.geodesy.GlobalCoordinates;
import org.junit.Test;

/**
 - 经纬度求距离
 - @author huting
 */
@Slf4j
public class GeodesyTest {

    @Test
    public void testDistance(){
        GlobalCoordinates sourcePoint = new GlobalCoordinates(118.24255,24.33);
        GlobalCoordinates targetPoint = new GlobalCoordinates(118.24254,24.33);

        log.info("Sphere坐标系计算结果:[{}]",this.getDistance(sourcePoint,targetPoint,Ellipsoid.Sphere));
        log.info("WGS84 坐标系计算结果:[{}]",this.getDistance(sourcePoint,targetPoint,Ellipsoid.WGS84));
    }

    private double getDistance(GlobalCoordinates gpsFrom, GlobalCoordinates gpsTo, Ellipsoid ellipsoid){
        GeodeticCurve geodeticCurve = new GeodeticCalculator().calculateGeodeticCurve(ellipsoid,gpsFrom,gpsTo);
        return geodeticCurve.getEllipsoidalDistance();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 随机数

返回:java中常见方法总结

# javafaker

返回:java中常见方法总结

<!-- https://mvnrepository.com/artifact/com.github.javafaker/javafaker -->
<dependency>
    <groupId>com.github.javafaker</groupId>
    <artifactId>javafaker</artifactId>
    <version>1.0.0</version>
</dependency>
1
2
3
4
5
6
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.github.javafaker.Faker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Locale;

@RestController
@RequestMapping("/random")
public class RandomDataEndpoint {

    @Autowired
    private ObjectMapper objectMapper;

    @GetMapping("/persons")
    public JsonNode getRandomPersons() {

        Faker faker = new Faker();
        ArrayNode persons = objectMapper.createArrayNode();

        for (int i = 0; i < 10; i++) {
            persons.add(objectMapper.createObjectNode()
                    .put("firstName", faker.name().firstName())
                    .put("lastName", faker.name().lastName())
                    .put("title", faker.name().title())
                    .put("suffix", faker.name().suffix())
                    .put("address", faker.address().streetAddress())
                    .put("city", faker.address().cityName())
                    .put("country", faker.address().country()));
        }

        return persons;
    }

    @GetMapping("/books")
    public JsonNode getRandomBook() {

        Faker faker = new Faker(new Locale("en-US"));
        ArrayNode books = objectMapper.createArrayNode();

        for (int i = 0; i < 10; i++) {
            books.add(objectMapper.createObjectNode()
                    .put("author", faker.book().author())
                    .put("genre", faker.book().genre())
                    .put("publisher", faker.book().publisher())
                    .put("title", faker.book().title()));
        }

        return books;
    }

    @GetMapping("/foods")
    public JsonNode getRandomFoods() {

        Faker faker = new Faker(new Locale("de"));
        ArrayNode foods = objectMapper.createArrayNode();

        for (int i = 0; i < 10; i++) {
            foods.add(objectMapper.createObjectNode()
                    .put("ingredients", faker.food().ingredient())
                    .put("spices", faker.food().spice())
                    .put("measurements", faker.food().measurement()));
        }

        return foods;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 你的随机数够随机吗

# 什么时随机数

随机数的随机性检验可以分为三个标准:

  • 统计学随机性。 统计学伪随机性指的是在给定的随机比特流样本中,1的数量大致等于0的数量,同理,“10”“01”“00”“11”四者数量大致相等。
  • 密码学安全伪随机性(不可预测性 )。 其定义为,给定随机样本的一部分和随机算法,不能有效的演算出随机样本的剩余部分。
  • 真随机性(不可重现性)。 其定义为随机样本不可重现。除非将数列本身保存下来,否则不能重现相同的数列。

# 各种语言中的随机数

  • java
    • java.util.Random() / Math.random() / java.util.concurrent.ThreadLocalRandom():使用线性同余方法,是非密码学安全的随机数。
    • java.Security.SecureRandom:产生的是密码学安全的随机数。
    • 同时也可以用NativePRNGBlocking或NativePRNGNonBlocking方法(NativePRNGBlocking使用/dev/random;NativePRNGNonBlocking使用/dev/urandom)

# 生成定长数字字符随机串

返回:java中常见方法总结

/**
* 生成6位随机数密码
* @param maxPwd 最大数字密码
* @return 密码
*/
private String generateNumberPwd(Integer maxPwd) throws NoSuchAlgorithmException {
    Validate.notNull(maxPwd,"数字随机密码的最大可能值不可为空");
    final int pwdLength = ObjectUtils.getDisplayString(maxPwd).length();
    SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
    final int pwd = secureRandom.nextInt(maxPwd+1);
    return String.format("%0"+pwdLength+"d",pwd);
}
1
2
3
4
5
6
7
8
9
10
11
12

nextInt(100);这行代码将生成范围0~100 之间的随机数,有趣的是,取值可能为 0 ,但不可能为 100。我们用中学数学课学习的区间表示法,表示为:[0, 100)

# 指定范围的随机数

int randNumber =rand.nextInt(MAX - MIN + 1) + MIN; // randNumber 将被赋值为一个 MIN 和 MAX 范围内的随机数
1

# BeanUtils

返回:java中常见方法总结

org.springframework.beans.BeanUtils,包含一个广为人知的 copyProperties 方法,于是点开这个类看了并没有转为map的
org.apache.commons.beanutils.BeanUtils有一个 populate(bean, map),的方法

PersonModel personModel = new PersonModel("male",13,"ht");
        Map<String,Object> map = Maps.newHashMap();
        BeanUtils.populate(personModel,map);
        log.info("map:[{}]",map);
1
2
3
4

测试时候发现map是空的,看源码:

/**
     - <p>Populate the JavaBeans properties of the specified bean, based on
     - the specified name/value pairs.</p>
     *
     - <p>For more details see <code>BeanUtilsBean</code>.</p>
     *
     - @param bean JavaBean whose properties are being populated
     - @param properties Map keyed by property name, with the
     -  corresponding (String or String[]) value(s) to be set
     *
     - @throws IllegalAccessException if the caller does not have
     -  access to the property accessor method
     - @throws InvocationTargetException if the property accessor method
     -  throws an exception
     - @see BeanUtilsBean#populate
     */
    public static void populate(final Object bean, final Map<String, ? extends Object> properties)
        throws IllegalAccessException, InvocationTargetException {

        BeanUtilsBean.getInstance().populate(bean, properties);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

就是说先要把 bean 的属性设为 map 的 key 才行,有 key 才会有值,而 new 的 map 当然是空的,结果还是空的。

PersonModel personModel = new PersonModel("male",13,"ht");
        Map<String,Object> map = Maps.newHashMap();

        log.info("map:[{}]",BeanUtils.describe(personModel));
1
2
3
4
12:41:36.428 [main] INFO com.chlm.mysession.common.StringTest - map:[{country=null, firstName=null, lastName=ht, createTime=null, sex=male, updateUser=null, createUser=null, updateTime=null, id=null, class=class com.chlm.mysession.entity.PersonModel, version=null, age=13}]
1

# LRU

返回:java中常见方法总结

LRU(Least Recently Used),也就是最近最少使用。一种有限的空间资源管理的解决方案,会在空间资源不足的情况下移除掉最近没被使用过的数据,以保证接下来需要的空间资源。

# 其性能问题

返回:java中常见方法总结

# Base64原理

返回:java中常见方法总结

很早之前,电子邮件刚刚问世,那时候消息的传递都是英文,后来中国开通了互联网之后,对邮件的使用量也大量增加,这时候电子邮件就有了中文的需求。但是中文在传输的时候不能被有效地处理,这时候Base就出来了,Base64通过对这些中文进行编码,转化为服务器和网关能够识别的数据。这时候就能够使用电子邮件有效地传输了。比如说网络上传递图片,我们可以Base64先对图片进行处理,然后就可以有效的传输了。

# Base64编码

Base64的原理超级简单,相信我们都知道ASCII 编码,从A-Z、a-z、0-9和一些其他的特殊字符,这些字符都有唯一的一个数字来表示。比如说a是97,A是65。
同理Base64也有这样一套编码。范围是”A-Z“、”a-z“、”0-9“、”+“、”/“一共64个字符
由于索引是从0开始,所以最后的索引是63。在编码的时候Base64就是通过上面的进行转换编码的。

# 基本原理

比如说有一封邮件,我们想要对其使用Base64进行编码。怎么办呢?基本步骤如下:

  • (1)对邮件的数据进行切分,每三个字节一组,一共24个bit。
  • (2)对切分后的数据重组,24个bit重组为4组,每组6个bit。
  • (3)对重组后的数据处理,每组最前面添加两个“0”,构成每组8个bit。此时一共32个bit。
  • (4)根据Base64编码表,获取相应的编码值。

此时一封完整的邮件,被切分重组处理之后就变成了Base64编码了。

# 实例验证

比如说三个字母sky。我们要对这个三个字符使用Base64进行编码。

  • (1)对邮件的数据进行切分,每三个字节一组,一共24个bit

s:115-->01110011
k:107-->
y:121-->

  • (2)对切分后的数据重组,24个bit重组为4组,每组6个bit
  • (3)对重组后的数据处理,每组最前面添加两个“0”,构成每组8个bit。由于在最前面添加的0,所以对数值不构成影响。
  • (4)根据Base64编码表,获取相应的编码值
  • (5)完成编码的转换

有些地方需要我们去注意一下:
(1)在第三步中,最前面添加了两个0,所以最终编码之后要比之前多出三分之一的大小。
(2)上面的例子中,我们使用的是ASCII编码,但是如果我们使用UTF-8,对应Base64编码的结果是不一样的。
(3)Base64只是进行了编码,方便数据的传输而已。这可不是加密。

public void testBase64() throws IOException {
        String name = "sky";
        BASE64Encoder base64Encoder = new BASE64Encoder();
        String encodeStr = base64Encoder.encode(name.getBytes());
        System.out.println("编码结果:"+encodeStr);
        BASE64Decoder base64Decoder = new BASE64Decoder();
        byte[] decodeBytes = base64Decoder.decodeBuffer(encodeStr);
        System.out.println("解码结果:"+new String(decodeBytes));
    }
1
2
3
4
5
6
7
8
9

# 前后端get&set

返回:java中常见方法总结

实体类定义如下属性时

private Integer vSerialNumber;
1

此时通过lombok生成get时,其函数名称为getVSerialNumber();
此时前端获取此属性时,必须写为vserialNumber,如果写成vSerialNumber则获取不到,
但如果后台返回的是一个Map时,其key值为vSerialNumber时,则前端获取属性的值时则直接写成vSerialNumber

# 注入属性

返回:java中常见方法总结

# System.getProperties() 获取系统环境变量

在工作中经常遇到获取系统环境变量,如获取当前资源路径、获取当前用户名等等,那如何获取所有的系统变量呢?通过System.getProperties()就可以了。

// 获取当前资源路径
System.getProperty("user.dir");
// 获取当前用户名
System.getProperty("user.name");
1
2
3
4

# @Value注解支持属性值注入

在 Spring 组件中使用 @Value 注解的方式,可以很方便的读取 properties 文件的配置值。

@Value的属性值注入有两类:
${ property : default_value }
#{ obj.property? : default_value }

第一个注入的是外部参数对应的property
第二个则是SpEL表达式对应的内容。
那个 default_value,就是前面的值为空时的默认值。注意二者的不同。

  • @Value 注解的使用场景很多,包括:
    • 在声明的变量中使用;
    • 在setter方法中;
    • 普通方法和构造方法中;

下面看看几个简单的示例:

@Value("${username:rickie}")
private String userName2;
1
2

如果username 没有设置值,userName2变量值将会设置为默认值rickie。

@Value("${user.name:tom}")
private String userName;
1
2

读取系统环境变量user.name,一般而言,系统环境变量user.name 都会有值,因此userName 变量将会设置为系统环境变量的值(用户名)。

@Value("${user.age:25}")
private Integer age;
1
2

给包装类型Integer或者简单类型int 设置默认值。

@Value("${some.key:one,two,three}")
private String[] stringArrayWithDefaults;
@Value("${some.key:1,2,3}")
private int[] intArrayWithDefaults;
1
2
3
4

数组的默认值可以使用逗号分割。

@Value("#{systemProperties['user.name'] ?: 'default username'}")
private String spelWithDefaultValue;
1
2

使用 Spring Expression Language (SpEL) 设置默认值。
上面的代码表示在systemProperties中,如果没有设置 user.name 的值,my default system property value 会被设置成默认值。

使用 Spring @Value 为属性设置默认值。在项目中,提供合理的默认值,在大多情况下不用任何配置,就能直接使用。达到零配置的效果,降低被人使用的门槛。简化新Spring应用的搭建、开发、部署过程。

# 完整代码示例

返回:java中常见方法总结

下面的代码,整合了通过System.getProperties()获取系统环境变量和@Value注解注入属性值。

@RestController
public class TestController {
@Value("${user.name:tom}")
private String userName;

@Value("${username:rickie}")
private String userName2;

@Value("${user.age:25}")
private Integer age;

@Value("#{systemProperties['user.name'] ?: 'my default system property value'}")
private String spelWithDefaultValue;

@RequestMapping("/user")
public String simple() {
return "Hello " + userName + ", " + age + "!<br />"
+ "userName2: " + userName2 + "<br />"
+ spelWithDefaultValue + "<br />"
+ getProperties();
}

private String getProperties() {
        StringBuilder sb = new StringBuilder();
        Properties properties = System.getProperties();
        Iterator it = properties.entrySet().iterator();
        while(it.hasNext()){
        Map.Entry entry=(Map.Entry)it.next();
        Object key = entry.getKey();
        Object value = entry.getValue();
        sb.append(key +": "+value + "<br />");
        }
        return sb.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 三目运算

返回:java中常见方法总结

# 直接看题

private static void test1(){
    System.out.println(a==b?9.9:9);
    System.out.println(a==b?'a':98);
    System.out.println(a==b?'a':Integer.MAX_VALUE);
    System.out.println(a==b?'a':b);
    System.out.println(a!=b?'a':b);

    Map<String, Long> map = Maps.newHashMaps();
    map.put("b",1L);
    System.out.println(map==null?-1L:map.get("a"));
}
1
2
3
4
5
6
7
8
9
10
11

计算

问:a=1,b=1时,test1()的执行结果?

答案:9.0 b 2147483647 2 97 Exception

类型一致

在使用三目运算符时,尽量保证两个返回值的类型一致,不然会触发类型转换,转换规则:

  • 如果返回值 X 和返回值 Y 是同种类型,那么返回类型毫无疑问就是这种类型。
  • 如果两个返回值 X 和 Y 的类型不同,那么返回值类型为他们两最接近的父类

举个栗子

举例:// String 和 Boolean 都实现了 Serializable 接口 Serializable serializable = a == b ? "true" : Boolean.FALSE; // 所有类都继承了 Object 类 Object o = a == b ? new ArrayList<>() : new TernaryOperatorDemo();

  • 对于基本数据类型,如果其中一个返回值 X 类型为byte、short或者char,另一个返回值 Y 类型为int,那么若在编译期就能判断出 Y 的取值范围在 X 的取值范围之内,则返回类型为 X 的类型,反之则为 Y 的类型。如果返回值 X 类型不为以上几种,则会触发隐藏类型转换。
  • 基本数据类型和对象数据类型相遇时,三目运算符默认返回结果为基本数据类型

# 反爬虫、接口防盗刷 spring boot stater 组件 kk-anti-reptile

返回:java中常见方法总结

kk-anti-reptile是,适用于基于spring-boot开发的分布式系统的反爬虫组件。

# 系统要求

  • 基于spring-boot开发(spring-boot1.x, spring-boot2.x均可)
  • 需要使用redis