Java LocalTime.parse working in spring boot on local PC but not working in Docker container

Advertisements

Title describes my problem. I’m feeling really dumb now because it’s probably a minor, tiny issue.

I’ve been stuck on this for almost 2 days. The app I’m building is much bigger, with docker compose to start the postgres container, but the problem can be recreated by creating a new (java 21, maven) spring boot project with only spring web as its dependency. In the class with @SpringBootApplication, add the following:

@Bean
ApplicationRunner applicationRunner() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a");
        return args -> {
            System.out.println("Parsed: 12:30 pm -> " + LocalTime.parse("12:30 p.m.", formatter));
        };
}

When I run it with intellij or ./mvnw spring-boot:run, or in just a regular java class (without the annotation) it runs.

But, when I use this docker file:

FROM eclipse-temurin:21-jdk-alpine
COPY . /app/
WORKDIR /app
CMD ["./mvnw", "spring-boot:run", "-Dskiptests"]

And build with docker build -t localtime:1.0 ., then run with: docker run -p 1234:8080 --name localtime-container localtime:1.0. This is what I get after the regular spring startup log messages:

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-02-20T06:08:00.097Z ERROR 71 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.time.format.DateTimeParseException: Text '12:30 p.m.' could not be parsed at index 6
        at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2108) ~[na:na]
        at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:2010) ~[na:na]
        at java.base/java.time.LocalTime.parse(LocalTime.java:473) ~[na:na]
        at org.example.localtimedemo.LocaltimeDemoApplication.lambda$applicationRunner$0(LocaltimeDemoApplication.java:22) ~[classes/:na]
        at org.springframework.boot.SpringApplication.lambda$callRunner$4(SpringApplication.java:786) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:83) ~[spring-core-6.1.3.jar:6.1.3]
        at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-6.1.3.jar:6.1.3]
        at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:88) ~[spring-core-6.1.3.jar:6.1.3]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:786) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:774) ~[spring-boot-3.2.2.jar:3.2.2]
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:na]
        at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:na]
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:341) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.2.jar:3.2.2]
        at org.example.localtimedemo.LocaltimeDemoApplication.main(LocaltimeDemoApplication.java:15) ~[classes/:na]

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  26.863 s
[INFO] Finished at: 2024-02-20T06:08:00Z
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.2.2:run (default-cli) on project localtime-demo: Process terminated with exit code: 1 -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

BUTTTT, When I uncomment the sout statement, it runs fine.

I’ve tried using different Dockerfiles, same problem.

Does anyone know what’s going on and how to fix it?

Thank you

>Solution :

tl;dr

DateTimeFormatter
    .ofPattern( … ) 
    .withLocale( Locale.CANADA_FRENCH ) ;

Specify locale

The indicator for day/night is a matter for localization. Some locales use AM/PM. Some use am/pm. Some use a.m./p.m. And still others use I-don’t-know-what.

Your code neglects to specify a Locale object. So your DateTimeFormatter implicitly relies upon the JVM’s current default locale.

I would bet that the current default locale of the JVM in your own machine differs from that of your Docker environment.

The best solution is to avoid needlessly relying on the JVM’s current default locale. Specify explicitly.

To specify locale, call DateTimeFormatter#withLocale. Keep in mind that java.time uses immutable objects. So the withLocale method generates and returns a new fresh DateTimeFormatter object rather than altering (“mutating”) the original.

Locale locale = Locale.US ;
DateTimeFormatter f = 
    DateTimeFormatter
        .ofPattern( … ) 
        .withLocale( locale ) ;
String output = myLocalTime.format( f ) ;

That is just one example. You will need to determine for yourself the appropriate locale that meets your expectations.

Leave a ReplyCancel reply