记录一次奇怪的bug:因为循环注入导致@Value注入失败的问题

结论:
WebConfiguration活干的太多,注入了很多业务类(用户类),而业务类里面也通过@Autowired注入了
private SnowFlake snowFlake;导致在WebConfiguration加载中,需要先执行
@Bean
public SnowFlake snowFlake() {
  return new SnowFlake(workerId, datacenterId);
}
而此时
@Value("${snow.flake.worker.id}")
private long workerId;
@Value("${snow.flake.datacenter.id}")
private long datacenterId;
在业务类下面,还未注入成功,所以默认为0。
前言:

在接手维护一个别人的项目后,在启动日志里面,发现雪花算法的workerId和datacenterId一直都是0,与配置文件中的数字不一致。

Worker starting. timestamp left shift 11, datacenter id bits 3,
worker id bits 3, sequence bits 5, workerid 0 ,datacenterId 0

这个问题如果是多台服务器在业务繁忙时应该会出现id碰撞,导致部分数据插入失败。

过程:

先开始排查雪花算法问题。

public class SnowFlake {

    //下面两个每个5位,加起来就是10位的工作机器id
    private long workerId;    //工作id
    private long datacenterId;   //数据id

    //初始时间戳
    private long twepoch = 1577808000000L;

    //长度为5位
    private long datacenterIdBits = 3L;
    private long workerIdBits = 3L;
    //最大值
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    //序列号id长度
    private long sequenceBits = 5L;
    //序列号最大值
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    //初始化的序列号
    private long sequence = 1;
    //工作id需要左移的位数,12位
    private long workerIdShift = sequenceBits;
    //数据id需要左移位数 12+5=17位
    private long datacenterIdShift = sequenceBits + workerIdBits;
    //时间戳需要左移位数 12+5+5=22位
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    //上次时间戳,初始值为负数
    private long lastTimestamp = -1L;

    public SnowFlake(long workerId, long datacenterId) {
        this.workerId = workerId;
        this.datacenterId = datacenterId;

        // sanity check for workerId
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d ,datacenterId %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId, datacenterId);
        System.out.println("");
    }


    //下一个ID生成算法
    public synchronized long nextId() {
        long timestamp = timeGen();

        //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }

        //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        //将上次时间戳值刷新
        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    //获取时间戳,并与上次时间戳比较
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    //单次获取雪花id
    public static long getSnowflakeId(){
        SnowFlake worker = new SnowFlake(1, 1);
        return worker.nextId();
    }

    // 获取系统时间戳
    private long timeGen() {
        return System.currentTimeMillis();
    }

}

通过排查雪花算法的id发现没有什么问题,workerId和datacenterId值正常——通过下面回推结果正确。

    // 从雪花ID中提取时间戳
    public long extractTimestamp(long id) {
        return (id >> timestampLeftShift) + twepoch;
    }

    // 从雪花ID中提取工作机器ID
    public long extractWorkerId(long id) {
        return (id >> workerIdShift) & ((1L << workerIdBits) - 1);
    }

    // 从雪花ID中提取数据中心ID
    public long extractDatacenterId(long id) {
        return (id >> datacenterIdShift) & ((1L << datacenterIdBits) - 1);
    }

    public static void main(String[] args) {

        SnowFlake worker = new SnowFlake(1, 3);
        long id = worker.nextId();
        long timestamp = decoder.extractTimestamp(id);
        long workerId = decoder.extractWorkerId(id);
        long datacenterId = decoder.extractDatacenterId(id);
        System.out.println("");
        System.out.println("Timestamp: " + timestamp);
        System.out.println("Worker ID: " + workerId);
        System.out.println("Datacenter ID: " + datacenterId);
    }

继续排查,项目中SnowFlake的初始化是在WebConfiguration中通过@Bean注入的。如下代码所示:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private CustomerService customerService;

    @Autowired
    private ***Service ***Service;
    //...
    
    @Value("${snow.flake.worker.id}")
    private long workerId;
    @Value("${snow.flake.datacenter.id}")
    private long datacenterId;


    @Bean
    public CommonExceptionHandler centerExceptionHandler() {
        return new CommonExceptionHandler();
    }
    
    @Bean
    public SnowFlake snowFlake() {
        return new SnowFlake(workerId, datacenterId);
    }

    @Bean
    public JwtComponent jwtComponent() {
        return new JwtComponent(appName, 7 * 24 * 60 * 60 * 1000L, null);
    }

     
    @Bean
    public IdentityFilter identityFilter(JwtComponent jwtComponent) {
        return new IdentityFilter(workUnitAssembler(), jwtComponent
                , null
        );
    }

    @Bean
    public WorkUnitAssembler workUnitAssembler(){
        employeeService...
        customerService...
    }

}

通过调试和打印参数,得到每次 snowFlake() 是最先执行的,改变代码顺序也不影响执行顺序(PS:此时以为是调度问题,未能想到是注入顺序问题,导致后面浪费很多时间)。每次workerId, datacenterId都没有注解成功,默认值为0,在其他@Bean执行时workerId, datacenterId又都获取到值。此事this对象只有employeeService被注入成功,后面都为null。(PS:其实这时候也能判断出一些问题了)

当时猜测可能是SnowFlake执行顺序高于@Vlaue注解顺序。为了验证又重新编写了一个测试的类,如下

@Configuration
public class TestConfiguration {
    @Autowired
    private EmployeeService employeeService;
    @Value("${snow.flake.worker.id}")
    private long workerId;    //工作id
    @Value("${snow.flake.datacenter.id}")
    private long datacenterId;   //数据id

    private static final Integer test = 3;

    static {
        System.out.println("1 >> " + test);
    }

    public TestConfiguration() {
        System.out.println("2 >> " + workerId + "test:" + test );
    }

    @Bean
    public void test(){
        System.out.println("5 >>");
        employeeService.queryByUserId("1");

    }

    @Bean
    public JwtComponent jwtComponent() {
        System.out.println("4 >> " + workerId);
        return new JwtComponent("test", 7 * 24 * 60 * 60 * 1000L, null);
    }

    @Bean
    public SnowFlake snowFlake() {
        System.out.println("3 >> " + workerId);
        return new SnowFlake(workerId, datacenterId);
    }


    public TestConfiguration(long workerId) {
        this.workerId = workerId;
        System.out.println("6 >> " + workerId + "test:" + test);
    }

    public static void uTest() {
        System.out.println("7 >> ");
    }

}

得到的打印顺序是:

1 >> 3
2 >> 0 test:3
3 >> 2
4 >> 2
5 >>

通过打印顺序发现@Value注入并没有问题,那么推测可能是与WebConfiguration的一些问题相关。(PS:其实这里如果稍微转换思路就知道问题了,但是由于基础薄弱,还是没有发现根本问题,以为是别的底层原因。)

重新回到WebConfiguration中,继续排查。

如果将@Value放置在代码的执行上方,SnowFlake注入时又可以获取到值。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    
    @Value("${snow.flake.worker.id}")
    private long workerId;
    @Value("${snow.flake.datacenter.id}")
    private long datacenterId;
    @Autowired
    private EmployeeService employeeService;

    ...
}

到这里,当时感觉问题解决了。。。

当时表象是将@Value不能放置在多个(大于等于2)@Autowired后面

写到这里感觉还是代码逻辑问题,再编写TestConfiguration验证。

发现如果后面的Bean不实现@Autowired,那么不限定于1个或者多个,还是可以注入。如果在后面的@Bean都是后相互依赖的,可能是Spring以为暂时不需要注入影响了后面的@Value的注入。

@Configuration
public class TestConfiguration {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private CustomerService customerService;
    
    @Value("${snow.flake.worker.id}")
    private long workerId;    //工作id
    @Value("${snow.flake.datacenter.id}")
    private long datacenterId;   //数据id

    @Bean
    public SnowFlake snowFlake() {
        return new SnowFlake(workerId, datacenterId);
    }

    @Bean
    public JwtComponent jwtComponent() {
        return new JwtComponent("appName", 7 * 24 * 60 * 60 * 1000L, null);
    }

    @Bean
    public IdentityFilter identityFilter(JwtComponent jwtComponent) {
        return new IdentityFilter(workUnitAssembler(), jwtComponent
                , null // 日志收集
        );
    }
    @Bean
    public WorkUnitAssembler workUnitAssembler(){
        return identityWhole -> {
            if (identityWhole.getUserType() == UserTypeEnum.Employee.getCode()) {
                EmployeePO employeePo = employeeService.query(identityWhole.getUserId());

            } else if (identityWhole.getUserType() == UserTypeEnum.Customer.getCode()) {
                CustomerPO customerPo = customerService.query(identityWhole.getUserId());
            }
        };
    }
}

以为是

@Bean
public IdentityFilter identityFilter(JwtComponent jwtComponent) {
    return new IdentityFilter(workUnitAssembler(), jwtComponent
            , null // 日志收集
    );
}

影响了spring对@Value的注入。如果注释掉此处代码,spring会将所有@Autowired和@Value进行注入。应该是spring的管理策略导致的问题。

public class IdentityFilter extends OncePerRequestFilter {
    ...
}

到这里终于才要接近真相了。

----------------------------------------------------------分割线-----------------------------------------------------------

最后的原因是其他用户类中使用了,而员工类没有使用。(PS:潜意识一直以为都是一样的)

@Autowired
private SnowFlake snowFlake;

所以需要先注入

@Bean
public SnowFlake snowFlake() {
   return new SnowFlake(workerId, datacenterId);
}

同时workerId, datacenterId在这些用户类注入的下面,所以代码没有执行到赋值这步,所以默认为零。

参考文章:

【springboot源码】深度解析@Value赋值时机及底层原理

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值