写在前面

记录第一份实习收获到的技术,工具以及开发经验等

Jasypt加密

详细文章链接:https://planbbbbb.github.io/2024/01/20/Work-Jasypt%E5%8A%A0%E5%AF%86/

动态加密

在省级SaaS平台上多个省份,每个不同的地方可以根据自己的加密规则进行加密传输

同理,在程序启动时,根据不同省份的加密规则,进行解密

切换数据源

mybatis-flex自带的多数据源,在省级SaaS平台上多个省份(即多个租户),各个省份拥有自己独立的数据库源,

具体流程:

  1. 在yml文件中只配置了一个默认的数据源,即publicSaaS_rescue
  2. 在程序启动时运行init方法,去查表,获取所有租户的数据源信息,并将其存入Map中,key为数据库密码,即md5_sign,value为数据库名
  3. 在登录方法执行时,将数据源的信息传入,存到redis中,供后续使用
  4. 在其他方法执行时,在拦截器处会从redis里找出对应的数据源信息,并切换数据源
  5. 在本次方法结束后拦截器会清理threadlocal且切换为默认数据源,即publicSaaS_rescue
  6. 退出登录时就把redis对应的数据源信息删去

定时器

定时器可以在本地执行一些需要定时做的任务,例如每5秒在本地调用一个接口等。

这里把原本需要加锁来防止的线程安全的问题解决了:这个定时器每五秒就会去消息表里查询出来失败的,修改时间超过三分钟的,且发送次数小于三次的消息列表数据出来,然后在这个定时器中去进行这些消息的重复发送;

然而线程安全的问题就出现在这里了,因为去发送了一次post请求,假如时间很长,那么如果超过了五秒,因为定时器内是异步线程去处理的,所以等下一次五秒后,定时器再次触发,此时就会重复读到第一次定时器查询出来的那段数据列表,那么第一次和这一次查出来的列表数据就会出现重复的,毕竟数据并没有发生修改,故修改时间没有超过三分钟被查询了出来,那么两次分别的异步线程,就很有可能操作了同一个pushMsg对象,导致了其查询次数明明是加二,却因为线程竞争的问题导致查询次数只加了一,最后也在日志中证实了确实发生了该问题。

第一种解决方案就是给pushMsg对象进行加锁,即使下一次查询出来的相同的列表数据,也会因为锁而不会同时操作该对象,这样就保证了线程安全的问题。

可是这样的效果并不好,这样造成了线程大量的阻塞。

第二种方案是在查询出来该列表数据后,立刻批量地对这些数据进行次数加一,然后才用异步线程去发起post请求的调用,因为无论post请求的成功与否,这次请求都成立了,故可以直接批量加一,这样即使五秒后这些列表数据的各个异步线程并没有处理完这些post请求,下一次定时器也不会再查出相同的数据了。

这第二点的方案其实并不完全保证了线程安全,在极端情况下,假设第一次定时器查出来的数据列表很多导致还没有批量加一完,第二次定时器就已经触发了,那么就会出现上述的问题。但其实并不会出现改极端情况,毕竟五秒定时器一次,如果一直这样,那么可以确定地是每次查询出来地数据列表不会很多,也基本保证了能在五秒内完成批量加一操作,那么这样就避免了盲目的加锁导致的问题

版本一:

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
@Scheduled(cron = "*/5 * * * * ?")//每5秒执行一次
protected void pushMsg() {
boolean linux = System.getProperty("os.name").toLowerCase().contains("linux");
if (!linux) {
return;
}
System.out.println("定时器3");
List<PushMsg> pushMsgList = iDataDao.getPushMsgList();
for (PushMsg pushMsg : pushMsgList) {
new Thread(() -> {
// 提前修改访问次数和修改时间,防止下次定时器查询出相同的数据
iDataDao.upPushMsgNum(pushMsg);
String curDate = DateUtil.getCurDate();
SystemUtil.writeTxt(curDate + "执行上报异步处理");
SystemUtil.writeTxt(curDate + " " + pushMsg.getUrl_id() + " " + pushMsg.getRequest());
String result = HttpUtil.doPost(pushMsg.getUrl(), pushMsg.getRequest());
pushMsg.setResponse(result);
SystemUtil.writeTxt(curDate + " " + pushMsg.getUrl_id() + " " + result);
// int request_num = Integer.parseInt(pushMsg.getRequest_num());
// pushMsg.setRequest_num(String.valueOf(request_num + 1));
// pushMsg.setModified(DateUtil.getCurDate());
if (result != null && result.contains("200") && !result.contains("失败")) {
pushMsg.setFlag("0");
}
iDataDao.upPushMsg(pushMsg);
}).start();
}
}

版本二:

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
@Scheduled(cron = "*/5 * * * * ?")//每5秒执行一次
protected void pushMsg() {
boolean linux = System.getProperty("os.name").toLowerCase().contains("linux");
if (!linux) {
return;
}
System.out.println("定时器3");
List<PushMsg> pushMsgList = iDataDao.getPushMsgList();
// 提前批量修改访问次数和修改时间,防止下次定时器查询出相同的数据
iDataDao.upPushMsgNum(pushMsgList);
for (PushMsg pushMsg : pushMsgList) {
new Thread(() -> {
String curDate = DateUtil.getCurDate();
SystemUtil.writeTxt(curDate + "执行上报异步处理");
SystemUtil.writeTxt(curDate + " " + pushMsg.getUrl_id() + " " + pushMsg.getRequest());
String result = HttpUtil.doPost(pushMsg.getUrl(), pushMsg.getRequest());
pushMsg.setResponse(result);
SystemUtil.writeTxt(curDate + " " + pushMsg.getUrl_id() + " " + result);
if (result != null && result.contains("200") && !result.contains("失败")) {
pushMsg.setFlag("0");
}
iDataDao.upPushMsg(pushMsg);
}).start();
}
}

Mybatis拦截器和自定义分页注解

通过自定义了一个分页注解,在每一个需要进行分页的mapper方法上添加上这个注解,这样在xml里就不用自己写limit语句了,通过mybatis的拦截器拦截到除增删改外的查语句,即只会去对SELECT类型的去进行判断,而不会对INSERTUPDATEDELETE类型的语句去进行sql拼接。

每次查询的有分页注解的语句,都会进入拦截器,拦截器去做拼接sql,去把当前sql所查询的条数去记录到threadlocal中,方便查询只后再直接取出来存放到vo中。

TreeUtil

对于一些返回的id,他们有其对于的父id,称为pid(parentId),他们作为一个列表返回的时候,可以根据他们对应的关系进行类似于树状的展示。

自定义注解进行校验字段合法性

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
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyField {

/**
* 是否允许不为空
*/
boolean isNotNull() default true;

/**
* 是否是数值类型
*/
boolean fieldType() default false;


/**
* 默认数量
*/
int num() default -99;


/**
* 拼接
*/
String joint() default "";
}

通过AOP的切面找到post请求,此时通过反射去拿到校验字段再去判断字段的合法性

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
@Slf4j
@Aspect
public class Section {

/**
* 基于注解的切入点
*/
@Pointcut("execution(* com.planb.*.controller.*.*(..))")
public void aopAppController() {
}


@Before(value = "aopAppController()")
public void aopAppController(JoinPoint joinPoint) throws Exception {
//校验是否是POST请求
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String method = request.getMethod();
if (!"POST".equals(method)) {
return;
}
String typ = request.getHeader("content-type");
if (ValidateUtil.isNotBlank(typ) && typ.contains("json")) {
Object[] args = joinPoint.getArgs();
Object arg = args[0];
this.opVerifyField(arg.getClass(), arg);
}

}

//自定义注解校验
private void opVerifyField(Class<?> aClass, Object obj) throws Exception {
Field[] fields = aClass.getDeclaredFields();
for (Field item : fields) {
// 允许访问私有属性
item.setAccessible(true);
// 获取属性名
String name = item.getName();
// 获取属性值
Object value = item.get(obj);
//初始化数据
if (ValidateUtil.isBlank(value)) {
//赋值给属性值
item.set(obj, null);
}
//检验字段
VerifyField annotation = item.getAnnotation(VerifyField.class);
ApiModelProperty modelProperty = item.getAnnotation(ApiModelProperty.class);
if (ValidateUtil.isNotBlank(annotation)) {
if (!annotation.fieldType() && ValidateUtil.isBlank(value) && annotation.isNotNull()) {
if (ValidateUtil.isBlank(modelProperty)) {
throw new BaseUnCheckedException(name + "不能为空");
} else {
throw new BaseUnCheckedException(modelProperty.value() + "不能为空");
}
}
if (annotation.fieldType() && annotation.num() != -99) {
if (ValidateUtil.isBlank(value) || Integer.parseInt(value.toString()) <= 0) {
//赋值给属性值
item.set(obj, annotation.num());
}
}
String str = item.getType().toString();
if (ValidateUtil.isNotBlank(value) && ValidateUtil.isNotBlank(annotation.joint()) && str.contains("String")) {
value = value + annotation.joint();
//赋值给属性值
item.set(obj, value);
}
}
}
}

@Before("@annotation(limit)")
public void limit(Limit limit) {
ThreadLocalUtil.set(ThreadLocalConstant.LIMIT, "limit");
}
}

自定义注解实现分页

编写工具类

把每个接口的请求,返回的响应,这其中发生的报错,请求头,路径等相关信息从服务器日志里存储在excel表格中

代码规范

所有的equals方法,都要把常量放在前面,防止出现空指针的异常,因为会有旧版本的兼容问题,所以是不确定有些非常量值是否为空的

如下为规范写法:

1
"0".equals(dto.getIsChn())

启动时做处理

在spring boot项目启动时就可以去加载数据了,例如加载字典数据等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class ApplicationRunnerImpl implements ApplicationRunner {
@Autowired
private GeneralDataService generalDataService;

@Override
public void run(ApplicationArguments args) {
System.out.println("通过实现ApplicationRunner接口,在spring boot项目启动后打印参数");
String[] sourceArgs = args.getSourceArgs();
for (String arg : sourceArgs) {
System.out.print(arg + " ");
}
System.out.println();
this.generalDataService.loadData();

// 初始化 Excel 表格
ExcelInterceptor.initExcel();
}
}

在生成二维码的时候,用线程池,异步地去执行耗时长的二维码写入的操作

在项目启动时把字典数据这种常用的频繁的存到redis

自定义日志管理系统

在定时器模块写了同步工单的一个接口方法后,由于同步工单要这个方法要打印大量的sql语句,故对原有的日志管理进行优化。

原本的日志管理系统,只有api.log和error.log,分别记录全部的日志信息和只有错误的日志信息,同时同级目录下还有两个目录,分别是apis和errors这俩目录,分别用于记录五天之内的日志信息,具体为api.log文件每满100MB之后就进行日志的分片,按照当日日期+分片序号(自动生成)来命名,故一日可以有多个100MB的日志文件,具体采用的是利用文件大小滚动 SizeAndTimeBasedFNATP 来实现,这样更便于查找和管理。

现在的问题是由于同步工单的方法的sql打印过多,导致影响查看其他方法的日志,故进行统一的分类。

操作方法如下:

  1. 自定义一个注解,包含日志的级别和线程的名称
  2. 自定义一个AOP切面,然后基于这个注解的切点,去获取到线程的名称,然后进行修改该线程的线程名和日志级别
  3. 在logback-spring.xml文件中,使用MDC来区分不同的线程,为每个名称建立一个log文件和文件夹,文件夹里也是像上述的一样进行滚动切片存储5日内的日志。

参考文档:https://blog.csdn.net/SmartTxp/article/details/89114249