Spring BeanPostProcessor and inconsistently failing app startup

53 Views Asked by At

This is interesting.

I have my client's java app code dockerized. The maven build happens also through containers (a separate build.Dockerfile). If I, client, dev, whoever build the application locally, i.e. on our laptops, the app starts just fine. However, if we push to GitLab and run the build in GL's CI runners, the build passes no problem, But when it comes to launching the app -- it fails with an error:

***************************
APPLICATION FAILED TO START
***************************

Description:Parameter 1 of method method1 in my.app.Application required a bean of type 'my.infra.user.UserKind' that could not be found.


Action:

Consider defining a bean of type 'my.infra.user.UserKind' in your configuration.

If I download the created image to localhost and run it there -- it also fails. I've diff'ed everything I could in those containers, files are more or less identical. The only differences are time formatting and ordering. If I extract the jar alone from the image and try to run it - I get the same effects: the image built in GitLab fails to boot, while the locally built one passes. if I do jar -t on both the jars I see lots of differences. File paths are the same, but the order in which they are listed is different across both jars. This is the only considerable difference I've found.

Now about the code. Consider this example:

    @Bean("txManager")
    public PlatformTransactionManager buildTransactionManager(
            DataSource dataSource,
            @Named("MyUserKind") UserKind<Message> userManager,
            ApplicationStartupTransaction transaction
    ) {
    ...
    }

The tricky part is the BeanPostProcessor. It filters beans of type User (they are classes defined in code) and transforms them into another type -- UserKind.

Now, if the function parameter were

            @Named("MyUserKind") User<Message> userManager,

it would most likely always work: the bean name is clear and it matches the class definition in code (@Named("MyUserKind") @Component public class User {...}).

I hooked up the debugger and tried to make sense out of it tracing what Spring does. It's too complex for me to understand it well, but one thing I am sure of: when the application does NOT crash (local build), the UserKind bean is already created (post-processed) before invoking the @Bean method and sitting and waiting in to be used. In the cases where the application DOES crash, the dependency bean (UserKind) does not yet exist (i.e. is not created). The error goes away if I manually annotate the @Bean method with @DependsOn("MyUserKind") and specify (so now there are 2 mentions of it: one in the @DependsOn and another in @Named).

Again, this is an annoying issue, because it does not occur if I build the jar locally. It only occurs if I build it on GitLab runners. And even then the only difference between jars I can find is the ordering of items in the zip's index table (central directory).

Questions

  1. why does the order of entries in the built jar (zip) archives differ across platforms? Am I correct to assume that the factors influencing it are the backing filesystem and the OS? Let me remind you, that build happens in a container (multistage Dockerfile), so in theory the process should be the same, regardless of the platform. Why is the outcome different though?
  2. Am I correct to assume that this order of files in the jar archive influences the order in which Spring loads and initializes beans?
  3. Why isn't it enough to have @Named("MyUserKind") in the @Bean method params to make sure Spring finds and initializes all the required dependencies for that @Bean? My assumption is that @Named is married to the UserKind, i.e. when Spring looks up a bean by its name, it also needs the bean to have that type as well; now, the "MyUserKind" bean name directly maps not to the UserKind class, but to MyUser (@Named("MyUserKind") public class User {...}), and in order for it to become a UserKind it has to pass through the custom BeanPostProcessor implementation. Could it be that @Named only instructs to look up a bean definition as-is, i.e. w/o running it through the BPP, and @DependsOn is more involving, in the sense that it also triggers the BPP? I'm just guessing here.
  4. How do I advise the developer to get out of this mess? I don't think I can ensure a consistent ordering of jar contents on different platforms, so how can it be fixed in code? I'd personally vouch for KISS and avoiding type-mutating BPPs, but I'm not sure what other options we have apart from @DependsOn-tagging the whole beans' graph manually

EDIT I can indeed confirm that this has to do with the way files are ordered in the jar archive. In the pipeline I added a separate step to unzip the jar, list all the files, sort them and add them all back into a new zip (jar). Then I tried running this script locally and then launching the jar and now I'm getting the required a bean of type error at boot time.

0

There are 0 best solutions below