1️⃣开发随记

开发过程中对问题的记录
开发随记
type
status
date
slug
summary
tags
category
icon
password

集合去重

如果是单纯对一个对象整体,或者是其他包装类型的集合进行去重,可以使用HashSet或TreeSet。使用set去重需要注意,HashSet去重是通过对象的equals方法来进行区分的,所以一般需要重写对象的equals方法。但是也可以使用如下无侵入的方式:
  • HashSet根据某一字段去重
    • TreeSet
      如果是想要根据对象中某个字段来进行去重,可以使用Stream中的toMap方法, 利用map的键不可重复的特性,曲线救国:

      Arrays.asList注意事项

      使用asList将元素转为list时,要注意,转换后的list没有add和remove方法。因此无法执行add和remove操作。否则会报错。可以通过如下操作解决:

      Transactional注意事项

      在项目中遇到一个bug。
      就是以下代码中,邮件发送的逻辑是异步执行的。但是整个 apply 方法被 @Transactional 注解标记,这个方法中所有的数据库操作都会等到事务执行完毕后才真正提交到数据库。因此就导致当代码执行到邮件发送的逻辑后,邮件发送逻辑中如果需要从数据库获取数据,会无法获取到,因为此时可能异步的任务执行到获取数据那一步的时候,业务代码中的事务并未提交,导致数据没有入库。
      业务代码:
      producer:
      consumer:
      解决方案:
      在事务提交之后执行邮件发送逻辑
      • 可以考虑使用 TransactionSynchronizationManager.registerSynchronization 实现 afterCommit 方法,让邮件发送的逻辑在事务提交之后执行
      • 也可以手动提交事务。但是如果手动提交事务,那么 @Transactional 注解会失效,就需要手动回滚事务。

      BUG排查思路

      500异常

      1. 打开控制台,查看报错的接口是哪个
      1. 观察发送的请求,是否有重复请求?考虑部分接口在重复调用的情况下是否会出现无法预料的异常?
      1. 观察payload和response,观察数据结构是否正确
      1. 打开后端代码,进行调试,排错

      日期转换

      Java中的日期转换

      1. DateLocalDateTime
        1. 首先将date转为Instant,再指定时区
      1. LocalDateTimeDate
        1. LocalDateTime 先指定时区,转为 Instant, 再通过from转为date
      1. LocalDate to Date
        1. LocalDate 只包含日期信息,没有时间信息。 这里我们使用 atStartOfDay() 方法将它设置为当天开始的时间 (00:00:00),然后按照与 LocalDateTime 相同的方式转换为 Date。 同样,这里也使用了系统默认时区。

      LocalDateTime序列化问题

      LocalDateTime类型数据在序列化为JSON数据时,会被默认转化为时间戳的格式。想要让其以正常格式输出,可以使用 @JsonFormat 注解。

      序列化问题

      定时任务的持久化异常。因为实体类发生过变动,在序列化时,由于没有手动指定序列化id,而实体类发生变动(字段修改)后,类hash值发生改变,序列化id也发生了变化。所以quartz无法根据之前旧的序列化id找到对应的实体类。
      解决方案:为实体类手动定义序列化id
      [!NOTE]
      建议所有可序列化的类都显式声明serialVersionUID的值,因为默认的serialVersionUID计算对类的详细信息非常敏感,这些详细信息可能因编译器实现而异。因此在反序列化期间可能导致意外的错误(InvalidClassException)。因此为了保证不同java编译器反序列结果一致,可序列化的类强烈建议显示声明serialVersionUID的值,并建议使用private修饰符,因为此声明仅适用于立即声明的类,而不适用于其子类。

      数据库字段插入列表数据

      数据库中字段是 varchar ,而希望存储一个形如 [1,2323,545] 的列表数据。 实体类应该直接为 List<Integer> ,并用注解 @TableField(typeHandler = JacksonTypeHandler.class) 标记。
      然后在@TableName注解上开启autoResultMap属性即可。

      计算时间差

      Quartz

      三个重要角色:
      • job 具体的任务
      • trigger 触发器,决定了job将在何时触发
      • scheduler 调度器,是job和trigger的管理者和调度者,可以获取触发器状态,调用触发器,取消触发器。定时任务的一切操作都是使用 scheduler 完成的
      其他的看源码即可,源码简洁易懂。

      Server Send Event

      SSE的核心是 SseEmitter ,用以向客户端发送消息。
      当客户端和服务器建立连接之后,服务器会以 text/event-stream 的MIME格式返回一个 SseEmitter 给客户端,之后,客户端和服务器就会一直维持这个连接,直到时间达到 SseEmitter 设定的过期时间。
      SSE实践:
      1. 客户端通过调用一次服务器的接口来建立SSE的连接。
      1. 服务器向客户端推送数据。

      中文乱码

      SseEmitter 在SpringMVC中默认是ISO编码格式,此时若客户端采用的编码格式是UTF-8,则会导致中文乱码。因此需要重写 SseEmitterextendResponse 方法,手动设置其编码格式。

      并发编程

      使用 ThreadPoolExecutor 来显式创建线程池。因为这样可以手动指定核心线程数,最大连接数,拒绝策略等。不要使用 Executors 工具类的静态方法例如 newFixedThreadPool 来创建线程池。虽然这样更加方便,但是这样创建出来的线程池不利于维护和拓展。newFixedThreadPool 线程池是一个固定大小的线程池,并且使用的是无界阻塞队列,容易造成内存溢出。以下是 newFixedThreadPool 源码:
      应当使用 ThreadPoolExecutor 来显式创建线程池:
      线程池使用:

      线程池拒绝策略

      • DiscardOldestPolicy:丢弃最老的任务;
      • CallerRunsPolicy:同步调用, 遇到新任务会将任务返回给调用者线程,让其执行;
      • AbortPolicy:丢弃新任务并抛出异常;
      • DiscardPolicy:丢弃新任务;

      异步编排

      使用:

      多线程环境中的上下文传递问题

      今天线上出了一个问题,同事反映说会议预约时发送的邮件中显示的预约人信息不对,我一看,明明会议是A预约的,邮件中却显示是B。
      这个情况让我非常好奇,后来经过思考和排查,大致将问题锁定在并发安全的问题上。
      因为预约会议的业务执行过程中,发送邮件是交给线程池来执行的,在处理邮件发送的逻辑中有通过用户上下文获取当前登录用户信息的操作,我初步判断,可能是由于主线程和线程池中执行邮件发送线程之间的上下文没有按预期传递导致的问题。

      解决方案

      1. 直接传递用户id
        1. 在调用发送邮件方法时将主线程中的登录用户信息作为参数进行传递。
      1. 使用 transmittable-thread-local 作为存储用户上下文的容器,配合TtlExcutorsTtlRunnable 实现上下文在多线程环境中的传递。
        1. 使用Spring Security提供的 DelegatingSecurityContextRunnable
          1. 或者

        Spring 循环依赖

        spring通过三级缓存来解决循环依赖问题。
        一级缓存: singletonObjects 单例池,存放完全初始化好的bean
        二级缓存:earlySingletonObjects 早期bean,存放实例化,但是并未进行依赖注入和初始化的早期bean
        三级缓存:singletonFactories 存放bean工厂,用于创建早期引用
        假设A和B相互依赖,以下是A和B的创建流程:
        1. 创建A实例
            • 检查一级、二级、三级缓存,都没有A
            • 实例化A
            • 将A的 ObjectFactory 放入三级缓存
            • 依赖注入,发现依赖B
        1. 创建B实例
            • 检查一级、二级、三级缓存,都没有B
            • 实例化B
            • 将B的 ObjectFactory 放入三级缓存
            • 依赖注入,发现依赖A
        1. 解决循环依赖
            • 检查一级缓存,没有A
            • 检查二级缓存,没有A
            • 检查三级缓存,发现有A,获取A的早期引用
            • 将A的早期引用放入二级缓存,并将三级缓存中A的早期引用移除
            • B完成依赖注入
            • B初始化完成,放入一级缓存,清除二级、三级缓存中的B
        1. 完成 A 的创建:
            • A 得到了完全初始化的 B
            • A 完成属性注入和初始化
            • A 放入一级缓存 ,清除二、三级缓存中的 A
        1. 最终状态:
            • 一级缓存中有完全初始化好的 A 和 B
            • 二级缓存和三级缓存为空

        为什么需要三级缓存?

        先说结论,三级缓存的作用是为了优化性能。如果没有三级缓存,spring在实例化A的时候,并不知道这个A需不需要使用aop(aop的执行时机是在bean初始化完成之后),那这种情况下,sping是创建A的原始对象放入二级缓存呢?还是创建A的代理对象放入缓存呢?
        • 如果创建原始对象放入缓存,后面发生循环依赖时,B注入的就是A原始对象,但是当A初始化完成后,开始执行aop逻辑时,又需要创建代理对象并放入一级缓存。这就导致了B中注入的是原始对象,其他地方获取A的时候,获取的是代理对象。这有可能会导致aop失效。
        • 如果一开始就为所有的bean创建代理对象并放入二级缓存,会造成资源的浪费。
        三级缓存的设计,就是为了延迟创建bean的早期引用,在需要时才决定返回代理对象还是原始对象:
        1. 创建Bean A:
            • 实例化A,得到A的原始对象
            • 将A的ObjectFactory放入三级缓存:singletonFactories.put("A", () -> getEarlyBeanReference(A))
            • 开始填充A的属性,发现依赖B
        1. 创建Bean B:
            • 实例化B,得到B的原始对象
            • 将B的ObjectFactory放入三级缓存
            • 开始填充B的属性,发现依赖A
            • 检查一、二级缓存没有A
            • 从三级缓存中获取A的工厂,调用getEarlyBeanReference,该方法可以判断A是否需要代理,如果A需要代理,此时就会创建代理对象 (循环依赖的特殊场景,提前创建代理对象)
            • 将得到的A(可能是代理对象)放入二级缓存,并从三级缓存中移除
            • B完成初始化,放入一级缓存
        1. 完成A的创建:
            • A注入了完整的B
            • A完成初始化
            • 检查A是否已经被提前代理过(通过标记),如果是,则不再创建新的代理
            • A放入一级缓存
        1. 最终结果:
            • B中注入的是A的早期引用(可能是代理对象)
            • 其他地方获取A时,也是获取相同的对象
            • 保证了所有引用A的地方获取的都是同一个对象(统一都是代理对象或都是原始对象)
         
        上一篇
        logstash应用
        下一篇
        frp内网穿透
        Loading...