写在前面
记录第一份实习收获到的技术,工具以及开发经验等
Jasypt加密
详细文章链接:https://planbbbbb.github.io/2024/01/20/Work-Jasypt%E5%8A%A0%E5%AF%86/
动态加密
在省级SaaS平台上多个省份,每个不同的地方可以根据自己的加密规则进行加密传输
同理,在程序启动时,根据不同省份的加密规则,进行解密
切换数据源
mybatis-flex自带的多数据源,在省级SaaS平台上多个省份(即多个租户),各个省份拥有自己独立的数据库源,
具体流程:
- 在yml文件中只配置了一个默认的数据源,即publicSaaS_rescue
- 在程序启动时运行init方法,去查表,获取所有租户的数据源信息,并将其存入Map中,key为数据库密码,即md5_sign,value为数据库名
- 在登录方法执行时,将数据源的信息传入,存到redis中,供后续使用
- 在其他方法执行时,在拦截器处会从redis里找出对应的数据源信息,并切换数据源
- 在本次方法结束后拦截器会清理threadlocal且切换为默认数据源,即publicSaaS_rescue
- 退出登录时就把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 * * * * ?") 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);
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 * * * * ?") 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
类型的去进行判断,而不会对INSERT
,UPDATE
,DELETE
类型的语句去进行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 { 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();
ExcelInterceptor.initExcel(); } }
|
在生成二维码的时候,用线程池,异步地去执行耗时长的二维码写入的操作
在项目启动时把字典数据这种常用的频繁的存到redis
自定义日志管理系统
在定时器模块写了同步工单的一个接口方法后,由于同步工单要这个方法要打印大量的sql语句,故对原有的日志管理进行优化。
原本的日志管理系统,只有api.log和error.log,分别记录全部的日志信息和只有错误的日志信息,同时同级目录下还有两个目录,分别是apis和errors这俩目录,分别用于记录五天之内的日志信息,具体为api.log文件每满100MB之后就进行日志的分片,按照当日日期+分片序号(自动生成)来命名,故一日可以有多个100MB的日志文件,具体采用的是利用文件大小滚动 SizeAndTimeBasedFNATP
来实现,这样更便于查找和管理。
现在的问题是由于同步工单的方法的sql打印过多,导致影响查看其他方法的日志,故进行统一的分类。
操作方法如下:
- 自定义一个注解,包含日志的级别和线程的名称
- 自定义一个AOP切面,然后基于这个注解的切点,去获取到线程的名称,然后进行修改该线程的线程名和日志级别
- 在logback-spring.xml文件中,使用MDC来区分不同的线程,为每个名称建立一个log文件和文件夹,文件夹里也是像上述的一样进行滚动切片存储5日内的日志。
参考文档:https://blog.csdn.net/SmartTxp/article/details/89114249