Here is the English translation of the article body.
Are you trying to connect Spring Boot to MySQL and unsure what to put in application.yml?
I also often hear from people who managed to connect, but then saved timestamps are off by nine hours, Japanese text turns into ”???”, or the app crashes on startup with Public Key Retrieval is not allowed.
This article walks you through the whole process end to end: starting MySQL 8 with Docker Compose, adding the dependency, configuring the datasource in application.yml, and verifying the connection. After that, we go over what each JDBC URL parameter means and its recommended value, the common errors, and how to fix timezone and character-encoding mismatches.
If you got here by searching for an error message, feel free to jump straight to the “Common Errors and How to Fix Them” section.
Goals of This Article and Prerequisites
The prerequisites are as follows.
- Spring Boot 3.x (Java 21)
- MySQL 8.0 or 8.4
- JDBC driver:
com.mysql:mysql-connector-j
The driver coordinates have changed from the old mysql:mysql-connector-java to com.mysql:mysql-connector-j. Spring Boot 3.x does not manage the version of the old coordinates, so if you write them without a version, dependency resolution fails. When copying from older articles, replace them with com.mysql:mysql-connector-j.
The goal is four steps: start MySQL, add the dependency, write application.yml, and verify the connection. The datasource configuration is the same whether you use JPA or MyBatis, so you can read on regardless of which one you plan to use.
Starting MySQL 8 with Docker Compose
There are many ways to install MySQL, but here we focus on Docker Compose. The key point is to pin the character set and timezone from the very start.
# compose.yaml
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: appdb
MYSQL_USER: app
MYSQL_PASSWORD: secret
TZ: Asia/Tokyo
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
interval: 5s
timeout: 3s
retries: 10
volumes:
mysql-data:
If you specify MYSQL_DATABASE and MYSQL_USER, the database and a user with privileges on that database are created automatically on first startup. Because a named volume is attached, the data survives even if you remove the container. TZ works because the official mysql image includes tzdata; it may not take effect on lightweight image builds.
After docker compose up -d, check the contents once.
docker compose exec mysql mysql -uroot -proot \
-e "SHOW VARIABLES LIKE 'character_set_server'; SELECT @@global.time_zone, @@system_time_zone;"
If you see utf8mb4 and JST, you are good. Checking this up front means you won’t have to suspect the server side later when text gets garbled or times are off.
If you also want to run the Spring Boot app itself in a container, see How to Run a Spring Boot App in a Docker Container.
Adding the Dependency (Gradle / Maven)
runtimeOnly is sufficient for the driver. Your application code only depends on the JDBC API, so there is no need to expose the driver’s implementation classes at compile time. Leave the version to Spring Boot’s dependency management.
// build.gradle
dependencies {
// JPAを使うなら
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// MyBatisを使うならこちら(どちらか一方でOK)
// Boot の依存管理外なので、Maven Central で最新の 3.0.x を確認して指定する
// implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
runtimeOnly 'com.mysql:mysql-connector-j'
}
For Maven, it looks like this.
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
Whichever you choose, JPA or MyBatis, the datasource configuration that follows is the same.
Minimal application.yml Configuration
The minimal configuration needed to connect is these three items.
spring:
datasource:
url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8
username: app
password: ${DB_PASSWORD:secret}
driver-class-name is not specified. Spring Boot automatically detects com.mysql.cj.jdbc.Driver from the jdbc:mysql: prefix in the URL. There is no harm in specifying it explicitly, but leaving out what works without it keeps the configuration smaller.
The password is read from an environment variable in the form ${DB_PASSWORD}. Adding a default value like ${DB_PASSWORD:secret} is convenient, but leaving it in production YAML invites accidents, so limit it to development.
The parameters appended to the URL are explained in the next section.
JDBC URL Parameters: Meaning and Recommended Values
Here is a summary of the commonly used parameters in Connector/J 8.x. useUnicode=true and serverTimezone, which appear in older articles, are now either unnecessary or treated as aliases.
| Parameter | Meaning | Recommended value | Use |
|---|---|---|---|
connectionTimeZone | The reference timezone JDBC uses for date/time conversion | Asia/Tokyo or UTC | Both |
forceConnectionTimeZoneToSession | Also set the session time_zone to the same value | true (if you use the TIMESTAMP type) | Both |
characterEncoding | Connection character encoding. UTF-8 is treated as utf8mb4 | UTF-8 | Both |
sslMode | SSL handling. Successor to useSSL | Dev DISABLED / Prod REQUIRED or stricter | Both |
allowPublicKeyRetrieval | Allow retrieving the server public key over non-SSL connections | true in dev only | Dev |
createDatabaseIfNotExist | Create the database if it doesn’t exist (requires CREATE privilege) | true in dev only | Dev |
serverTimezone remains only as an alias for connectionTimeZone since Connector/J 8.0.23, so use connectionTimeZone when writing new configuration. Likewise, useSSL has been replaced by sslMode, and if both are specified, sslMode takes precedence.
createDatabaseIfNotExist requires the connecting user to have the global CREATE privilege. The app user created via MYSQL_USER only has privileges on MYSQL_DATABASE, so attempting to create a database with a different name results in Access denied ... to database.
Here are the finished forms for development and production side by side.
spring:
datasource:
# 開発用
url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
# 本番用(コメントを外して差し替える)
# url: jdbc:mysql://db.example.internal:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=REQUIRED
For production, it is enough to remember: remove allowPublicKeyRetrieval and createDatabaseIfNotExist.
Verifying the Connection (SELECT 1 and Table Creation)
First, let’s verify in a way that depends on neither JPA nor MyBatis. JdbcTemplate is included in both starters, so you can use it as is.
@Component
public class ConnectionCheckRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(ConnectionCheckRunner.class);
private final JdbcTemplate jdbcTemplate;
public ConnectionCheckRunner(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void run(String... args) {
Integer one = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
String tz = jdbcTemplate.queryForObject("SELECT @@session.time_zone", String.class);
log.info("SELECT 1 = {}, session time_zone = {}", one, tz);
}
}
If HikariPool-1 - Start completed. appears in the startup log, the connection itself has succeeded. If SELECT 1 = 1 follows, the connectivity check is complete. How to use JdbcTemplate is covered in How to Run Plain SQL with JdbcTemplate in Spring Boot.
The minimal additional configuration for JPA and MyBatis is just this.
spring:
jpa:
hibernate:
ddl-auto: update # 開発用。本番では validate か none
show-sql: true
# MyBatisの場合はこちら
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
With ddl-auto: update, you can just write one @Entity and start the app, and the table gets created. Leaving it as update in production is risky because entity changes get applied to the schema automatically, so set it to validate or none and leave schema management to Flyway, described later. For how to write Mappers, see How to Use MyBatis with Spring Boot.
Common Errors and How to Fix Them
For those who arrived by searching for an error message, here is a short summary of causes and fixes.
Public Key Retrieval is not allowed
The cause is the combination of MySQL 8’s default authentication plugin caching_sha2_password and a non-SSL connection (sslMode=DISABLED). In development, either add allowPublicKeyRetrieval=true or enable SSL to fix it.
The server time zone value 'JST' is unrecognized or represents more than one time zone.
The cause is that the server’s system_time_zone is an abbreviation like JST that Connector/J cannot interpret. This mainly occurs with drivers 8.0.22 or earlier, or with older configurations that don’t specify serverTimezone. From 8.0.23 onward, the default for connectionTimeZone is LOCAL (the JVM’s timezone), and the server side is only consulted when you specify connectionTimeZone=SERVER, so it does not occur by default. Check that you haven’t copied dependencies from an old article, and to be safe, explicitly set connectionTimeZone=Asia/Tokyo (or serverTimezone for older drivers).
Communications link failure
The cause is a wrong hostname or port, or the MySQL container not having finished starting up. From an app inside Compose, connect using the service name mysql, and add condition: service_healthy to depends_on so the app waits for startup.
Access denied for user 'app'@'172.18.0.1'
The cause is a wrong password or the source host not being allowed. The user created via MYSQL_USER is 'app'@'%', but if you manually create 'app'@'localhost', connections from outside the container are rejected.
Unknown database 'appdb'
The cause is that the database has not been created. Either create it via MYSQL_DATABASE, or add createDatabaseIfNotExist=true with a user that has the CREATE privilege.
Why Timestamps Are Off and How to Fix It
“I can connect, but the saved timestamps are off by nine hours” is a really common question. Keep in mind that there are three layers involved.
- The JVM’s default timezone (
user.timezone) - The MySQL server and session
time_zone - Connector/J’s conversion reference (
connectionTimeZone)
On top of that, MySQL’s TIMESTAMP type is stored in UTC and converted using the session time_zone, whereas the DATETIME type is stored as is without conversion. This is why “I inserted the same value but got different results depending on the column type.”
On the Java side, the clearest mapping is LocalDateTime (no conversion) for DATETIME columns, and Instant for values you want to treat as absolute points in time, mapped to TIMESTAMP columns. OffsetDateTime is intended for API boundaries where you want to pass values with an offset. Note that MySQL has no type that retains the offset, so it is converted to a TIMESTAMP equivalent when stored. With JPA, you can also pin the reference Hibernate uses when passing values to JDBC via spring.jpa.properties.hibernate.jdbc.time_zone.
The JVM layer can be pinned at startup with java -Duser.timezone=Asia/Tokyo -jar app.jar, or with the environment variable TZ=Asia/Tokyo in a container. When in doubt, either align all three (server, JVM, and connection) to the same timezone, or set everything to UTC. When things are off, you can check the current state with SELECT @@session.time_zone, NOW(). The check queries are collected in the next section.
Why Japanese Text Gets Garbled and How to Fix It
For garbled text, likewise, look at two places: the server side and the connection side.
On the server side, check character_set_server and the CHARACTER SET / COLLATION of the database and tables. If you pinned utf8mb4 in the Docker Compose command, databases and tables created afterward inherit it automatically. On the connection side, use characterEncoding=UTF-8, which Connector/J maps to utf8mb4.
One thing to watch out for is that MySQL’s utf8 is actually an alias for utf8mb3, which only handles up to three bytes. If an old table was created with utf8, it fails with Incorrect string value the moment you insert an emoji. Check existing tables with SHOW CREATE TABLE, and if they are utf8mb3, convert them to utf8mb4.
-- タイムゾーンの確認
SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone, NOW();
-- 文字コードの確認と変換
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE users;
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
The Relationship with HikariCP and the maxLifetime Setting
We haven’t configured the pool at all so far, but Spring Boot uses HikariCP by default, so you are already connecting through a connection pool. The HikariPool-1 in the startup log is the proof.
With MySQL, the one setting I want you to pay attention to is max-lifetime. MySQL closes idle connections on the server side once they exceed wait_timeout (default 28800 seconds). If HikariCP’s lifetime is longer than that, the isValid() check at checkout fails and you keep seeing the WARN log Failed to validate connection ... Possibly consider using a shorter maxLifetime value. If the server closes the connection during query execution, you get Communications link failure. HikariCP’s default is 30 minutes, so it is usually not a problem, but in environments where the server-side wait_timeout has been shortened, make sure to set it below that value.
spring:
datasource:
hikari:
max-lifetime: 570000 # wait_timeout=600 なら数十秒短くする
maximum-pool-size: 10
You sometimes see configurations with connection-test-query: SELECT 1, but Connector/J supports JDBC4’s isValid(), so it is unnecessary. Specifying it actually makes validation slower. Calculating pool size and monitoring with Actuator are covered in detail in How to Properly Configure and Tune the HikariCP Connection Pool in Spring Boot.
Managing the Schema with Flyway
ddl-auto: update is handy, but it is not suited to production schema management. If you leave it to Flyway, note that for MySQL, flyway-core alone is not enough; you also need flyway-mysql. Without it, the app fails on startup with Unsupported Database: MySQL.
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
At the same time, set spring.jpa.hibernate.ddl-auto to validate, dividing responsibilities so that Flyway manages the schema and Hibernate only validates it. Then just place a single src/main/resources/db/migration/V1__create_users.sql, and it is applied automatically at startup. If the Flyway version managed by Spring Boot is old relative to MySQL 8.4, you’ll see Flyway upgrade recommended: MySQL 8.4 is newer than this version of Flyway, but this is a warning, not a failure, so no need to panic. Versioning practices and the production rollout flow are summarized in Database Migration Management with Flyway in Spring Boot.
Production Configuration (SSL and Switching Profiles)
To avoid carrying the development URL straight into production, separate the settings by profile.
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
jpa:
hibernate:
ddl-auto: update
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=REQUIRED
# 証明書まで検証する場合。CA証明書を入れたトラストストアが必要
# url: jdbc:mysql://${DB_HOST}:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=VERIFY_CA&trustCertificateKeyStoreUrl=file:/etc/app/ca-truststore.p12&trustCertificateKeyStorePassword=${TRUSTSTORE_PASSWORD}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
sslMode=REQUIRED encrypts the communication but does not verify the server certificate. If you raise it to VERIFY_CA or VERIFY_IDENTITY, you need to register the CA certificate that issued the server certificate in a truststore. You can do this either by specifying a PKCS12 truststore with trustCertificateKeyStoreUrl, or by importing the CA into the JVM’s cacerts (since fallbackToSystemTrustStore defaults to true, the latter is consulted without adding anything to the URL). With a private CA such as RDS or Cloud SQL, setting VERIFY_CA without registering the certificate makes the connection fail with PKIX path building failed.
There are three key points for production. Set sslMode to REQUIRED or stricter, remove allowPublicKeyRetrieval and createDatabaseIfNotExist, and inject the password from environment variables or a secrets manager instead of writing it in YAML. For how to switch profiles, see How to Safely Switch Environment-Specific Configuration Using Spring Boot Profiles.
Summary
The stumbling points when connecting Spring Boot to MySQL mostly boil down to three: “can’t connect,” “times are off,” and “text is garbled.” Connection problems are solved with URL parameters (allowPublicKeyRetrieval and sslMode), time offsets by aligning the three timezone layers of JVM, server, and connection, and garbled text by standardizing on utf8mb4.
Make life easy in development by adding sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true to the URL, and in production, set sslMode=REQUIRED or stricter and remove those parameters. As long as you stick to this switch, you should have almost no trouble with connectivity.
Once you are connected, move on to Implementing CRUD in a REST API or How to Use data.sql and schema.sql for loading initial data.