Spring-boot: can't inject `local.server.port` into a @Configuration so it can be wired into a @Component

Created on 17 Oct 2018  路  11Comments  路  Source: spring-projects/spring-boot

So the short of my problem is, I'm trying to make a graphql client library, and test to see that it's working. @LocalServerPort isn't becoming available until @Test in Junit 5 in my project. I can't quite repro this in a demo project, but I believe this project should work, as is

@SpringJUnitConfig
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT )
class ActuatorApplicationTest {

    @Autowired String url;

    @Test
    void port() {
        Assertions.assertNotNull( url );
    }

    @TestConfiguration
    static class Config {
        @Bean
        String url( @LocalServerPort int port ) {
            return "http://localhost:" + port;
        }
    }
}

This is my real code right now...

@Configuration
class ApolloConfig {

    @Bean
    @Profile( "!test" )
    static ApolloClient.Builder prodClient( @Value( "${phdb.endpoint}" ) String graphqlEndpoint ) {
        return ApolloClient.builder().serverUrl( graphqlEndpoint );
    }

    @Bean
    @Profile( "test" )
    static ApolloClient.Builder testClient(@Value( "${local.server.port}" ) int port ) {
        return ApolloClient.builder();
    }
}

failure is

 Unexpected exception during bean creation; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'local.server.port' in value "${local.server.port}"
invalid

Most helpful comment

I stumbled across this 'issue' as well - got it working if somebody needs it in the future.

  @Lazy // allows for initialization after the server port is known
  @TestConfiguration(proxyBeanMethods = false)
  static classTestConfiguration {

    @Bean
    public YourClass yourBean(@Value("${local.server.port}") int port) {
      //...
    }
  }

All 11 comments

as a side note, I don't understand the need for local.server.port why not just use server.port seems like the latter is what I'd generally expect, but because it's different I also have to make a double config.

server.port on a RANDOM_PORT test is 0.

@spencergibb yes, but that's not right... and not really helpful to the problem... just saying it doesn't seem like a proper dependency injection since I can't use the same definition, and have to wire it twice.

https://github.com/spring-projects/spring-boot/blob/80da9cf5ebdc256c0b743fd0c34c451a368530aa/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServerPortInfoApplicationContextInitializer.java#L62-L66

this code seems really late in the startup process which is likely why my expectations aren't being met.

The port isn鈥檛 guaranteed to be known until the embedded container has started and bound its connector. You can inject it into a test class by which time the context will have been refreshed. Alternatively, if you need it earlier you can listen for the WebServerInitializedEvent. You can鈥檛 inject it into any component as the port is not known in time.

that also doesn't help andy... I need to set up a custom http client to talk to the server... what you're telling me is that it's impossible to do that, it shouldn't be impossible to do that.

you basically just said you can't inject jdbc into your repository/dao because even though we create it we don't know the port in time...

I haven't said it's impossible, just that your expectation about local.server.port isn't realistic. You hadn't really described what you're trying to do and it's hard to reverse engineer from a few code snippets.

We've solved what I think is a similar problem for the auto-configured TestRestTemplate using LocalHostUriTemplateHandler. You might want to take a look at that and use it as inspiration.

If you need further guidance, then please ask on Stack Overflow or Gitter.

I will try to clarify (and look at the link in a second), I have a hand crafted repo that talks to a graphql api-ed database... I want to inject the http client in this case is apollo. In order to test this I'm spinning up a fake graphql server in spring boot. However, in order to configure the client correctly, I need a valid URI, which means I need the port. The only documented way to get the port is with local.server.port but that doesn't work as I would expect most @Value's to work, seeing that it doesn't seem it can actually be injected.

@Component
class EMRRepoImpl implements EMRRepo {
    private final ApolloClient client;

    EMRRepoImpl( ApolloClient client ) {
        this.client = Objects.requireNonNull( client );
    }
````

my test (currently a little different as I've been debugging)

@ActiveProfiles("test")
@SpringJUnitConfig
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT )
class EMRRepoTest {

private final EMRRepo repo;
private final ApolloClient.Builder builder;
private final Environment env;
@Autowired
EMRRepoTest( EMRRepo repo, ApolloClient.Builder builder, Environment env ) {
    this.repo = repo;
    this.builder = builder;
    this.env = env;
}

```

is expecting to inject the repo, and that needs to be autowired anyways because it's a component. Essentially the same position I'd be in if I wanted to inject TestWebClient instead of WebClient

I stumbled across this 'issue' as well - got it working if somebody needs it in the future.

  @Lazy // allows for initialization after the server port is known
  @TestConfiguration(proxyBeanMethods = false)
  static classTestConfiguration {

    @Bean
    public YourClass yourBean(@Value("${local.server.port}") int port) {
      //...
    }
  }

Thx @knoobie! Awesome! @Lazy in combination with a @Configuration saved my day.

I solved it with a kind of proxy bean

@Component
public class GraphQLClient {

    private ApolloClient apolloClient;
    private final Environment environment;

    public GraphQLClient(Environment environment) {
        this.environment = environment;
    }

    public ApolloClient getApolloClient() {
        if (apolloClient == null) {
            String port = environment.getProperty("local.server.port");
            initApolloClient(port);
        }
        return apolloClient;
    }

    public synchronized void initApolloClient(String port) {
        this.apolloClient = ApolloClient.builder()
                .serverUrl("http://localhost:" + port + "/graphql")
                .build();
    }

    public <D extends Operation.Data, T, V extends Operation.Variables> GraphQLCallback<T> graphql(Operation<D, T, V> operation) {
        GraphQLCallback<T> graphQLCallback = new GraphQLCallback<>();
        if (operation instanceof Query) {
            Query<D, T, V> query = (Query<D, T, V>) operation;
            getApolloClient()
                    .query(query)
                    .enqueue(graphQLCallback);
        } else {
            Mutation<D, T, V> mutation = (Mutation<D, T, V>) operation;
            getApolloClient()
                    .mutate(mutation)
                    .enqueue(graphQLCallback);

        }
        return graphQLCallback;
    }
}
Was this page helpful?
0 / 5 - 0 ratings