后端百宝箱

随机生成验证码

1
2
//        调用hutool的工具,随机生成6位验证码
String code = RandomUtil.randomNumbers(6);

验证手机号,邮箱等是否合法的正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";

根据正则表达式的验证方法

1
2
3
4
5
6
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}

非Spring组件如何注入依赖

法一:

正常注入,在调用时用带有@Bean的方法调用

1
2
3
4
public class LoginInterceptor implements HandlerInterceptor {

@Autowired
private StringRedisTemplate redisTemplate;

调用:

1
2
3
4
@Bean
public LoginInterceptor authenticationInterceptor() {
return new LoginInterceptor();
}

法二:

构造器注入

1
2
3
4
5
private StringRedisTemplate redisTemplate;

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

在Spring环境中调用并传入

1
2
3
@Autowired
private StringRedisTemplate stringRedisTemplate;
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))

Redis工具类封装

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
@Component
@Slf4j
public class CacheClient {

private final StringRedisTemplate stringRedisTemplate;

public CacheClient(StringRedisTemplate redisTemplate) {
this.stringRedisTemplate = redisTemplate;
}


public void set(String key, Object value, Long time, TimeUnit unit){
/**
* @Description: 封装缓存存储工具
* @Params: [key, value, time, unit]
* 键,值,时间,时间单位
* @Return void
*/
String data = JSONUtil.toJsonStr(value);

stringRedisTemplate.opsForValue().set(key,data,time,unit);

}

public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
/**
* @Description: 利用逻辑时间存储,防止缓存击穿
* @Params: [key, value, time, unit]
* 键,值,时间,时间单位
* @Return void
*/

// 这里存放的value应该是同包下RedisData类
RedisData data = new RedisData();
data.setData(value);
data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

String jsonStr = JSONUtil.toJsonStr(data);
stringRedisTemplate.opsForValue().set(key,jsonStr);
}

public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type ,Function<ID,R> dbFallback,Long time,TimeUnit unit){
/**
* @Description: 读数据并防止缓存穿透
* @Params: [key, id, type, dbFallback,time,unit]
* 键,查询id,转换类型,数据库查询方法,过期时间,时间类型
* @Return R
*/
String key = keyPrefix + id;

String json = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(json)){
// 命中,则直接返回
R bean = JSONUtil.toBean(json, type);
return bean;
}
if(json != null){
// 命中空数据
return null;
}
// 没命中则查询数据库
R r = dbFallback.apply(id);
if(r == null){
// 存入空数据
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
throw new RuntimeException("未命中,内存存入空数据");
}
// 存入缓存
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time,unit);
return r;
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type ,Function<ID,R> dbFallback,Long time,TimeUnit unit){
/**
* @Description: 读数据并防止缓存击穿
* @Params: [key, id, type, dbFallback,time,unit]
* 键,查询id,转换类型,数据库查询方法,过期时间,时间类型
* @Return R
*/
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isBlank(json)){
return null;
}

// 命中,则转换
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
Object data = redisData.getData();
R r = JSONUtil.toBean((JSONObject) data, type);
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
// 未过期,直接返回
return r;
}
// 过期了,则调用线程,更新缓存
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);

if(isLock){

// 二次验证,防止重复重建
// json = redisTemplate.opsForValue().get(key);
// if(StrUtil.isBlank(json)){
// return null;
// }
//
//// 命中,则转换
// redisData = JSONUtil.toBean(json, RedisData.class);
// data = redisData.getData();
// R reR = JSONUtil.toBean((JSONObject) data, type);
// expireTime = redisData.getExpireTime();
// if(expireTime.isAfter(LocalDateTime.now())){
//// 未过期,直接返回
// return reR;
// }

// 验证失败,申请线程,重建缓存
CACHE_REBUILD_EXECUTOR.submit(() ->{
try{
// 重建缓存
// 查询数据库
R r1 = dbFallback.apply(id);
// 写入缓存
this.setWithLogicalExpire(key,r1,time,unit);
}catch (Exception e){
throw new RuntimeException(e);
}finally{
// 释放锁
unLock(key);
}
});

}
// 返回旧数据
return r;
}


private boolean tryLock(String key){
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);

return BooleanUtil.isTrue(b);
}

// 释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}

}
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
72
73
74
75
76
77
78
79
80
81
82
@Resource
private RedisTemplate redisTemplate;

private static final String CACHE_KEY_SEPARATOR = ".";

/**
* 构建缓存key
*/
public String buildKey(String... strObjs) {
return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
}

/**
* 是否存在key
*/
public boolean exist(String key) {
return redisTemplate.hasKey(key);
}

/**
* 删除key
*/
public boolean del(String key) {
return redisTemplate.delete(key);
}

/**
* set(不带过期)
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}

/**
* set(带过期)
*/
public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
}

/**
* 获取string类型缓存
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}

public Boolean zAdd(String key, String value, Long score) {
return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
}

public Long countZset(String key) {
return redisTemplate.opsForZSet().size(key);
}

public Set<String> rangeZset(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}

public Long removeZset(String key, Object value) {
return redisTemplate.opsForZSet().remove(key, value);
}

public void removeZsetList(String key, Set<String> value) {
value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
}

public Double score(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}

public Set<String> rangeByScore(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
}

public Object addScore(String key, Object obj, double score) {
return redisTemplate.opsForZSet().incrementScore(key, obj, score);
}

public Object rank(String key, Object obj) {
return redisTemplate.opsForZSet().rank(key, obj);
}

数据库密码加密

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
  datasource:
username: root
password: mEp5QZ5jrCO/G4PI+/K1C8jK9k8IzR3tr/R5TyLsGxE4dlkFcQs/UdVG/JKKnqVGG94F0dDC2w/SnFHP2CL39Q==
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:/***?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true
type: com.alibaba.druid.pool.DruidDataSource
# druid配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: 123456
filter:
stat:
enabled: true
log-slow-sql: true
slow-sql-millis: 2000
wall:
enabled: true
config:
# 开启配置
enabled: true
connection-properties: config.decrypt=true;config.decrypt.key=${publicKey}; #密码解密
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKhxct4yqOKXCvJO4NuTqhiszWmFVS+I6Zo6r2eqAqa2nQo4MniASZ3hvILYTi5v3o+jTxKchknMK6KXDUGtLhECAwEAAQ==

不过这样由于publicKey(公钥)是直接放在配置文件中的,所以实际还可能需要将publicKey存到某个文件夹下,或者手动输入。

加密代码:

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
public class DruidEncryptUtil {

private static String PRIVATEKEY;
private static String PUBLICKEY;

static{
try {
String[] keyPair = ConfigTools.genKeyPair(512);
PRIVATEKEY = keyPair[0];
PUBLICKEY = keyPair[1];
log.debug("private: {} public: {}",PRIVATEKEY,PUBLICKEY);
// System.out.println("private:" + PRIVATEKEY + "public: " + PUBLICKEY);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (NoSuchProviderException e) {
throw new RuntimeException(e);
}
}

public static String encrypt(String plainText) throws Exception {
// 加密
String encrypt = ConfigTools.encrypt(PRIVATEKEY, plainText);
log.debug("加密后:{}",encrypt);
// System.out.println("加密后:" + encrypt);
return encrypt;
}

public static String decrypt(String publicKey, String encryptText) throws Exception {
// 解密
String decrypt = ConfigTools.decrypt(publicKey, encryptText);
log.debug("解密后: {}",decrypt);
return decrypt;
}

public static void main(String[] args) throws Exception {
// 获得加密后的代码,以及解码公钥
encrypt("****");
}
}

统一返回类Result

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
@Data
public class Result<T> {

private Boolean success;

private Integer code;

private String message;

private T data;

public static Result ok(){
Result result = new Result();
result.setSuccess(true);
result.setCode(ResultCodeEnums.SUCCESS.code);
result.setMessage(ResultCodeEnums.SUCCESS.desc);
return result;
}

public static <T> Result ok(T data){
Result result = new Result();
result.setSuccess(true);
result.setCode(ResultCodeEnums.SUCCESS.code);
result.setMessage(ResultCodeEnums.SUCCESS.desc);
result.setData(data);
return result;
}

public static Result fail(){
Result result = new Result();
result.setSuccess(false);
result.setCode(ResultCodeEnums.FAIL.code);
result.setMessage(ResultCodeEnums.FAIL.desc);
return result;
}

public static <T> Result fail(T data){
Result result = new Result();
result.setSuccess(false);
result.setCode(ResultCodeEnums.FAIL.code);
result.setMessage(ResultCodeEnums.FAIL.desc);
result.setData(data);
return result;
}
}

code枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum ResultCodeEnums {
SUCCESS(200,"成功"),
FAIL(500,"失败");

public int code;
public String desc;

ResultCodeEnums(int code,String desc){
this.code = code;
this.desc = desc;
}

public static ResultCodeEnums GetEnumByCode(int code){
for(ResultCodeEnums resultCodeEnums : ResultCodeEnums.values()){
if(resultCodeEnums.code == code){
return resultCodeEnums;
}
}
return null;
}
}

log4j日志配置文件

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="INFO" monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} %X{PFTID} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定义日志存储的路径 -->
<property name="FILE_PATH" value="../log" />
<property name="FILE_NAME" value="jcClub.log" />
</Properties>

<!--https://logging.apache.org/log4j/2.x/manual/appenders.html-->
<appenders>

<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</console>

<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<File name="fileLog" fileName="${FILE_PATH}/temp.log" append="false">
<PatternLayout pattern="${LOG_PATTERN}"/>
</File>

<!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>

<!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>

<!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>

</appenders>

<!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
<appender-ref ref="fileLog"/>
</root>
</loggers>

</configuration>

分页处理封装类

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
@Data
public class PageResult<T> {

private int pageNo = 1;
private int pageSize = 10;
private int total = 0;
private int totalPages = 0;
private List<T> result = Collections.emptyList();
private int start = 1;
private int end = 0;

public void setRecords(List<T> result){
this.result = result;
if(result != null && result.size() > 0){
setTotal(result.size());
}
}

public void setTotal(int total){
this.total = total;
if(this.pageSize > 0){
this.totalPages = total / this.pageSize + (total % this.pageSize == 0?0:1 );
}else{
this.totalPages = 0;
}

this.start = (this.pageSize > 0?(this.pageNo - 1) * this.pageSize : 0) + 1;
this.end = (this.start - 1 + this.pageSize * (this.pageNo > 0 ? 1:0));

}

}