2025/05/12

Spring Boot 3 Data 2

Spring Data JPA

JPA: Java Persistence API,提供 POJO 物件持久化的標準規格,可把 Java 物件直接映射到資料庫中

JPA 是規格,Hibermate, TopLink, OpenJPA 都是 JPA 的實作。目前以 Hibernate 為 Java 最流行的 JPA framework。

Spring Data JPA 底層也是使用 Hibernate,最後透過 JDBC 連接資料庫。

spring-boot-starter-data-jpa 自動設定了 JPA,自動設定類別是

  • JpaRepositoriesAutoConfiguration

  • HibernateJpaAutoConfiguration

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-community-dialects</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

application.yml

spring:
  application:
    name: data2

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/testweb
    username: root
    password: password

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        # dialect: org.hibernate.dialect.MySQLDialect
        dialect: org.hibernate.community.dialect.MySQLLegacyDialect

傳統的 JPA 必需要設定 persistence.xml,但在 spring boot 不需要。

Entity class 只需要有 @Entity@SpringBootApplication 或是 @EnableAutoConfiguration 就會自動掃描所有 @Entity

Data2Application 直接啟動即可

@SpringBootApplication
public class Data2Application {

    public static void main(String[] args) {
        SpringApplication.run(Data2Application.class, args);
    }

}

UserController.java

package com.test.data2.controller;

import com.test.data2.entity.UserDO;
import com.test.data2.repo.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class UserController {

    private final UserRepository userRepository;

    @GetMapping("/user/info/{id}")
    public UserDO getUserInfo(@PathVariable("id") long id){
        UserDO userDO = userRepository.findById(id).orElseGet(null);
        return userDO;
    }

}

UserRepository.java

package com.test.data2.repo;

import com.test.data2.entity.UserDO;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserDO, Long> {

}

UserDO.java

package com.test.data2.entity;

import jakarta.persistence.*;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
@Entity(name = "user")
public class UserDO implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String phone;

    @Column(name = "create_time", nullable = false)
    private Date createTime;

    @Column(nullable = false)
    private Integer status;

}

啟動後,可從網址 http://localhost:8080/user/info/2 取得 userid = 2 的資料

console 會出現 sql

Hibernate: select ud1_0.id,ud1_0.create_time,ud1_0.phone,ud1_0.status,ud1_0.username from user ud1_0 where ud1_0.id=?

note:

如果掃描不到,可加上 @EntityScan 指定 package/class

Spring data JPA 的 db operation 稱為 repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.ListCrudRepository

  • org.springframework.data.repository.ListPagingRepository

  • org.springframework.data.repository.PagingAndSortingRepository

  • org.springframework.data.repository.kotlin.*

  • org.springframework.data.repository.reactive.*

UserRepository interface 會被自動掃描到,在 controller 能自動注入使用


MyBatis

MyBatis是一個Java持久化框架,它通過 XML 或 annotation 把物件與儲存程序或SQL語句關聯起來,對映成資料庫內對應的 record。

MyBatis-Plus 是 MyBatis 的封裝。


Redis

REmote DIctionary Server: Redis,是一種 key-value 的 NoSQL memory database。通常用在 cache 資料。

spring-boot-starter-data-redis

org.springframework.boot.autoconfigure.AutoConfiguration.imports 有註冊幾個自動設定類別

  • RedisAutoConfiguration

  • RedisReactiveAutoConfiguration

  • RedisRepositoriesAutoConfiguration

RedisAutoConfiguration 對應的參數綁定類別是 RedisProperties

在 application.yml 設定即可使用

spring:
  application:
    name: data3
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      password: password
#      client-type: jedis

RedisTemplate

有兩個預設的模板可直接使用

  • RedisTemplate

    可處理 <Object, Object>

  • StringRedisTemplate

    只能處理 <String, String> 的資料,預設使用 JDK 的 serializer

因為 StringRedisTemplate 繼承自 RedisTemplate,必須設定兩個 RedisTemplate,所以要搭配 @ConditionalOnSingleCandidate 注入

RedisTemplate 提供的 interface:

interface desc
GeoOperations 地理空間
HashOperations Hash data
HyperLogLogOperations HyperLogLog
ListOperations List
SetOperations Set
ValueOperations String
ZSetOperations Ordered Set

設定 pom.xml,要加上 spring-boot-starter-integration, spring-integration-redis

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

要新增一個連線,注入 StringRedisTemplate 使用

package com.test.data3;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class RedisController2 {

    private final StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/redis2/set")
    public String set(@RequestParam("name") String name, @RequestParam("value") String value) {
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        valueOperations.set(name, value);
        return valueOperations.get(name);
    }

}

呼叫網址 http://localhost:8080/redis2/set?name=test&value=12345 ,就會設定一個 redis key,可使用 redis-cli 檢查

> redis-cli -a password
127.0.0.1:6379> keys *
1) "test"
127.0.0.1:6379> get test
"12345"

也可以直接注入對應的 RedisTemplate 使用 valueOperations

@RequiredArgsConstructor
@RestController
public class RedisController3 {

//    private final StringRedisTemplate stringRedisTemplate;

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> valueOperations;

    @RequestMapping("/redis3/set")
    public String set(@RequestParam("name") String name, @RequestParam("value") String value) {
        valueOperations.set(name, value);
        return valueOperations.get(name);
    }

}

自訂 RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        RedisSerializer jacksonSerializer = getJacksonSerializer();

        template.setKeySerializer(stringSerializer);
        template.setValueSerializer(jacksonSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        template.setEnableTransactionSupport(true);
        template.afterPropertiesSet();

        return template;
    }

    private RedisSerializer getJacksonSerializer() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL);
        return new GenericJackson2JsonRedisSerializer(om);
    }
}

這裡 key 使用了 StringRedisSerializer, value 使用了 GenericJackson2JsonRedisSerializer

以下是幾個常用的 serializer

serializer desc
StringRedisSerializer String/byte[] 轉換器
JdkSerializationRedisSerializer JDK serializer
OxmSerializer XML serializer
Jackson2JsonRedisSerializer JSON serializer, 需要定義 JavaType
GenericJackson2JsonRedisSerializer JSON serializer, 不需要定義 JavaType

MongoDB

springboot 提供兩種 starter

  • spring-boot-starter-data-mongodb

  • spring-boot-starter-data-mongodb-reactive

首先修改 pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

在 org.springframework.boot.autoconfigure.AutoConfiguration.imports 註冊了幾個自動設定類別

  • MongoDataAutoConfiguration

  • MongoReactiveDataAutoConfiguration

  • MongoRepositoriesAutoConfiguration

  • MongoAutoConfiguration

  • MongoReactiveAutoConfiguration

設定 application.yml

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/test

使用 mongoTemplate

@RequiredArgsConstructor
@RestController
public class MongoController {

    public static final String COLLECTION_NAME = "test";
    private final MongoTemplate mongoTemplate;

    @RequestMapping("/mongo/insert")
    public User insert(@RequestParam("name") String name, @RequestParam("sex") int sex) {
        // add
        User user = new User(RandomUtils.nextInt(), name, sex);
        mongoTemplate.insert(user, COLLECTION_NAME);

        // query
        Query query = new Query(Criteria.where("name").is(name));
        return mongoTemplate.findOne(query, User.class, COLLECTION_NAME);
    }
}

新增 User.java

package com.test.data4;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;

@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(collection = "test")
public class User {
    private long id;
    private String name;
    private int sex;

}

發送網址 http://localhost:8080/mongo/insert?name=test&sex=1

就會 insert 一筆資料到 mongodb collection: test

/* 1 */
{
    "_id" : NumberLong(708252679),
    "name" : "test",
    "sex" : 1,
    "_class" : "com.test.data4.User"
}

MongoRepository

新增 UserRepository

package com.test.data4;

import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface UserRepository extends MongoRepository<User, Long> {

    List<User> findByName(String name);

}

修改 MongoController.java

    private final UserRepository userRepository;

    @RequestMapping("/mongo/insert2")
    public User repoInsert(@RequestParam("name") String name, @RequestParam("sex") int sex) {
        // add
        User user = new User(RandomUtils.nextInt(), name, sex);
        userRepository.save(user);

        // query
        return userRepository.findByName(name).get(0);
    }

發送網址 http://localhost:8080/mongo/insert2?name=test2&sex=1

就會新增 test2


Elasticsearch

springboot 支援3種客戶端

  • Elasticsearch 官方 REST 客戶端

  • Elasticsearch 官方 Java API Client

  • spring data elasticsearch 提供的 ReactiveElasticsearchClient

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

在 org.spingframework.boot.autoconfigure.AutoConfiguration.imports 註冊了

  • ElasticsearchDataAutoConfiguration

  • ElasticsearchRepositoriesAutoConfiguration

  • ReactiveElasticsearchRepositoriesAutoConfiguration

  • ElasticsearchClientAutoConfiguration

  • ElasticsearchRestClientAutoConfiguration

  • ReactiveElasticsearchClientAutoConfiguration

參數綁定 ElasticsearchProperties

參數 spring.elasticsearch.*

application.yml

spring:
  elasticsearch:
    uris: http://localhost:9200
    connection-timeout: 5s
    socket-timeout: 10s
    username: elastic
    password: password

這邊是用關閉 xpack,使用 http 的方式連線

改用 https 可參考

ref: spring-boot-starter-data-elasticsearch es带x-pack后台配置 - 微信公众号-醉鱼Java - 博客园


使用 ElasticsearchTemplate

EsController.java

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class EsController {

    public static final String INDEX_JAVA = "java";
    private final ElasticsearchTemplate elasticsearchTemplate;

    @RequestMapping("/es/insert")
    public User insert(@RequestParam("name") String name, @RequestParam("sex") int sex) throws InterruptedException {
        // add
        User user = new User(RandomUtils.nextInt(), name, sex);
        IndexCoordinates indexCoordinates =  IndexCoordinates.of(INDEX_JAVA);
        User save = elasticsearchTemplate.save(user, indexCoordinates);

        // delay and query
        Thread.sleep(1000l);
        Query query = new CriteriaQuery(Criteria.where("name").is(name));
        return elasticsearchTemplate.searchOne(query, User.class, indexCoordinates).getContent();
    }
}

User.java

@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "java")
public class User {
    private long id;
    private String name;
    private int sex;

}

使用 UserRepository

修改 EsController.java

    private final UserRepository userRepository;
    @RequestMapping("/es/repo/insert")
    public User repoInsert(@RequestParam("name") String name, @RequestParam("sex") int sex) {
        // add
        User user = new User(RandomUtils.nextInt(), name, sex);
        userRepository.save(user);

        // query
        return userRepository.findByName(name).get(0);
    }

UserRepository.java

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

public interface UserRepository extends ElasticsearchRepository<User, Long> {

    List<User> findByName(String name);

}

程式跟 MongoDB 類似

2025/05/05

Spring Boot 3 Data 1

spring boot 支援資料庫、connection pool、transaction 自動設定。資料庫還支援 H2, HSQL, Derby 嵌入式資料庫。NoSQL 支援 Mongodb, Neo4j, Elasticsearch, Redis, GemFire or Geode, Cassandra, Couchbase, LDAP, InfluxDB。比較常用的是 Redis, MongoDB, Elasticsearch。


嵌入式資料庫

存放在記憶體的資料庫

不需要設定 URL,只要引用 library 即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

資料來源 DataSource

java 是透過 javax.sql.DataSource 連接關聯式資料庫。spring boot 只需要引用 spring-boot-starter-data-jdbc 即可,另外還需要該關聯式資料庫的 JDBC driver,例如 MySQL 要用 mysql-connector

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

spring-boot-starter-data-jdbc 包含預設的 HikariCP connection pool

DataSource 自動設定的類別是 DataSourceAutoConfiguration

自動配置檔案 org.springframework.boot.autoconfigure.AutoConfiguration.imports

因為資料庫自動設定類別使用 @ConditionalOnClass,如果 classpath 裡面有以下兩個類別,就會自動完成設定

  • DataSource.class

  • EmbeddedDatabaseType.class

會先設定外部關聯式資料庫 DataSource,再設定嵌入式資料庫。如果沒有自動設定的類別,就會使用 connection pool 的預設設定值

application.yml 的設定參數為 spring.datasource.*

spring:
  application:
    name: data1

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    #driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost/testweb
    username: root
    password: password

也可以使用 MariaDB 的 driver

        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>

driver-class 也要同時修改

    driver-class-name: org.mariadb.jdbc.Driver

因 Dialect 在 MySQL 版本有差異,啟動 application 會出現這樣的錯誤資訊

org.hibernate.dialect.Dialect            : HHH000511: The 5.5.68 version for [org.hibernate.dialect.MySQLDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 8.0.0. Check the community dialects project for available legacy versions.

這時候要調整使用的 Dialect

        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-community-dialects</artifactId>
        </dependency>

application.yml

spring:
  jpa:
    properties:
      hibernate:
        # dialect: org.hibernate.dialect.MySQLDialect
        dialect: org.hibernate.community.dialect.MySQLLegacyDialect

啟動時,同時會看到 Hikari connection pool 的 log

HikariPool-1 - Starting...
HikariPool-1 - Added connection org.mariadb.jdbc.Connection@3a175162
HikariPool-1 - Start completed.

自訂 DataSource

直接註冊一個實作 DataSource 的 bean 即可

package com.test.data1;

import org.mariadb.jdbc.MariaDbDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration(proxyBeanMethods = false)
public class DbConfig {
    @Bean
    @ConfigurationProperties(prefix="spring.datasource")
    public DataSource dataSource() {
        return new MariaDbDataSource();
    }

}

也可定義兩個 DataSource,但第一個要設定為 @Primary

package com.test.data1;

import org.mariadb.jdbc.MariaDbDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration(proxyBeanMethods = false)
public class DbConfig2 {

    @Primary
    @Bean
    @ConfigurationProperties(prefix="spring.datasource.xx.one")
    public DataSource dataSource1() {
        return new MariaDbDataSource();
    }

    @Bean
    @ConfigurationProperties(prefix="spring.datasource.xx.two")
    public DataSource dataSource2() {
        return new MariaDbDataSource();
    }
}

透過 AbstractRoutingDataSource#determineCurrentLookupKey 決定哪一個 data source。


Connection Pool

spring boot 預設使用 HikariCP

自動設定類別是 PooledDataSourceConfiguration

spring 提供以下幾種 connection pool,依照 libary dependency 判斷順序選擇

  • HikariCP

  • Tomcat

  • DBCP2

  • Oracle UCP

如果都沒有,就直接使用JDBC connection

可設定 spring.datasource.type ,這樣就不會自動選擇 connection pool

spring:
  datasource:
    type: org.apache.commons.dbcp2.BasicDataSource

資料庫初始化

spring boot 可在啟動時處理 DDL, DML

目前是用 SqlInitializationAutoConfiguration 自動設定

對應參數綁定是 SqlInitializationProperties,可透過 spring.sql.init.* 參數設定

spring:
  application:
    name: data1

  datasource:
    #driver-class-name: com.mysql.cj.jdbc.Driver
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost/testweb
    username: root
    password: password
  sql:
    init:
      mode: ALWAYS
      continue-on-error: true
      schema-locations: sql/create_user.sql
      data-locations:
        - classpath:sql/insert_user.sql

mode: 資料庫初始化模式

  • ALWAYS: 會對外部關聯式資料庫初始化,application 每一次啟動,該 sql 都會被執行一次

  • EMBEDDED: 只會對 embedded database 自動初始化

  • NEVER

continue-on-error: true,初始化錯誤,要不要繼續執行。如果 mode 為 ALWAYS,就會忽略重複 insert 有 primary key 的資料的錯誤

schema-locations: DDL script

data-locations: DML script


JdbcTemplate

JdbcTemplate 是 spring 用來簡化 JDBC operation 的 wrapper,只需要引用 spring-boot-starter-data-jdbc 就可以使用

自動設定類別是 JdbcTemplateAutoConfiguration,裡面使用了 JdbcTempateConfiguration,有預設的 JdbcTemplate instance,在 application 中注入即可使用

@Slf4j
@RequiredArgsConstructor
@SpringBootApplication
public class Data1Application {
    public final JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        SpringApplication.run(Data1Application.class, args);
    }

    @Bean
    @Transactional
    public CommandLineRunner commandLineRunner() {
        return testJdbcTemplate();
    }

    private CommandLineRunner testJdbcTemplate() {
        return (args) -> {
            log.info("testing JdbcTemplate...");
            String username = jdbcTemplate.queryForObject("select username from user where id = 1", String.class);
            log.info("id = 1, username = {}", username);
        };
    }

}

自訂 JdbcTemplate

可以在 spring.jdbc.template.* 修改 jdbcTemplate 的設定

spring:
  jdbc:
    template:
      max-rows: 3
    private CommandLineRunner testJdbcTemplate2() {
        return (args) -> {
            log.info("testing JdbcTemplate2...");
            List<Map<String, Object>> result= jdbcTemplate.queryForList("select username from user");
            log.info("result list size={}", result.size());
        };
    }

因為設定的限制,回傳的 list size 最多只會是 3


transaction

spring-boot-starter-data-jdbc 會引用 transaction 相關 library,自動設定類別有

  • TransactionAutoConfiguration

  • DataSourceTransactionManagerAutoConfiguration

參數綁定類別是 TransactionProperties

spring.transaction.* 參數設定

新增一個 user DTO 物件

package com.test.data1.entity;

import lombok.Data;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@Data
public class UserDO {

    private long id;

    private String username;

    private String phone;

    @Column(value = "create_time")
    private LocalDateTime createTime;

    private int status;

}

製作 DAO interface UserDao.java

package com.test.data1.dao;

public interface UserDao {
    void update();
}

UserDaoImpl.java

package com.test.data1.dao.impl;

import com.test.data1.dao.UserDao;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class UserDaoImpl implements UserDao {

    public final JdbcTemplate jdbcTemplate;

    @Transactional
    @Override
    public void update() {
        jdbcTemplate.execute("update user set username = 'Petty' where id = 1");
        jdbcTemplate.execute("update user set username = 'Yoga' where id = 2");
        throw new RuntimeException("test exception");
    }

}

Data1Application.java

@Slf4j
@RequiredArgsConstructor
@SpringBootApplication
public class Data1Application {
    public final JdbcTemplate jdbcTemplate;

    public final UserDao userDao;

    public static void main(String[] args) {
        SpringApplication.run(Data1Application.class, args);
    }

    @Bean
    @Transactional
    public CommandLineRunner commandLineRunner() {
        return useJdbcTemplate();
    }

    private CommandLineRunner useJdbcTemplate() {
        return (args) -> {
            log.info("using JdbcTemplate...");
            BeanPropertyRowMapper<UserDO> rowMapper = new BeanPropertyRowMapper<>(UserDO.class);
            UserDO userDO = jdbcTemplate.queryForObject("select * from user where id = 2", rowMapper);
            log.info("user info : {}", userDO);

            List<UserDO> userDOList = jdbcTemplate.query("select * from user", rowMapper);
            log.info("user list: {}", userDOList);

            log.info("userDao.update()...");
            userDao.update();

            List<UserDO> userDOList2 = jdbcTemplate.query("select * from user", rowMapper);
            log.info("user list2: {}", userDOList2);
        };
    }
}

雖然在 update() 裡面修改了 username,但因爲 @Transactional 的關係,資料 rollback 回原本的狀態,修改的資料會被 rollback。


transaction 失敗的原因

  1. Database 不支援 transaction。MySQL 的 MyISAM 不支援 transaction,在 5.5 以後預設是使用 InnoDB,這個才有支援 transaction

  2. 沒有被 spring 管理

    UserDaoImpl 裡面的 @Service 如果刪除,就不會受到 spring 管理

    //@Service
    public class UserDaoImpl implements UserDao {
    }
  3. @Transactional 的 method 必須要是 public method

    如果要用在非 public method,必須開啟 AspectJ framework 的靜態代理模式

  4. 呼叫內部 method

    business method 一定要經過 spring 才能處理 transaction

    以下這兩種狀況,transaction 都不會生效

    @Service
    public class UserDaoImpl implements UserDao {
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional
       public void update2(User user) {
           //update user
       }
    }
    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional(propagation = Propagation.REQUIRES_NEW)
       public void update2(User user) {
           //update user
       }
    }

    解決方法是在 UserDaoImpl 裡面注入自己,用該物件,呼叫另一個 method

    @EnableAspectJAutoProxy(exposeProxy = true)

    取得當前的 proxy,並呼叫 method

    ((UserDaoImpl)AopContext.currentProxy()).update2();
  5. 沒有配置 transaction manager

    必須要有 PlatformTransactionManager 才能處理 transaction

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
       return new DataSourceTransactionalManager(dataSource);
    }

    spring-boot-starter-data-jdbc 裡面自動設定了一個 DataSourceTransactionalManager,所以可直接使用

  6. 設定不支援 transaction

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional(propagation = Propagation.NOT_SUPPORTED)
       public void update2(User user) {
           //update user
       }
    }
  7. 沒有 throw exception,就不會 rollback

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           try{
           } catch(Exception e) {
           }
       }
    }
  8. exception 類別不同

    因為 spring 預設針對 RuntimeException 進行 rollback,如果該 method throw 的 exception 類別錯誤,也不會觸發 rollback,除非在 @Transactional 設定 rollbackFor Exception 類別

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional(rollbackFor = Exception.class)
       public void update1(User user) {
           try{
           } catch(Exception e) {
              throw new Exception(e.getMessage());
           }
       }
    }

2025/04/21

SpringBoot3 Web 3

CORS 跨來源資源共享

SAMEORIGIN 同源政策,是 Netscape 提出,所有支援 Javascript 的瀏覽器都使用這個策略,同源是 domain name, port, protocol 相同的意思,網頁執行一個 script 時,會檢查是否同源,非同源就會拒絕。

只要 domain name, port, protocol 某一個不同,就代表 cross origin。

CORS 允許伺服器指示瀏覽器允許從除其自身以外的任何來源載入資源。

spring MVC 本身透過在 method 上設定 @CrossOrigin

@Slf4j
@RestController
@Validated
public class ResponseBodyController {

    @CrossOrigin
    @GetMapping(value = "/user/json/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public User getJsonUserInfo(@PathVariable("userId") @Size(min = 5, max = 8) String userId) {
        User user = new User("test1", 18);
        user.setId(Long.valueOf(userId));
        log.info("user info: {}", user);
        return user;
    }
}

@CrossOrigin 預設支援所有 request 來源、所有 http header, methods

@CrossOrigin 也可以用在 class

@Slf4j
@CrossOrigin(origins = "https://www.test.com", maxAge=3600)
@RestController
public class ResponseBodyController {
}

也可以放在 WebMvcConfigurer

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/user/**")
                .allowedMethods("GET", "POST")
                .allowedOrigins("https://www.test.com")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

安全性

加上 security library

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

啟動時會出現以下的 log

2024-11-20T16:36:50.740+08:00  WARN 5307 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 06ee988c-9983-4187-b040-5bfadde83893

This generated password is for development use only. Your security configuration must be updated before running your application in production.

2024-11-20T16:36:50.784+08:00  INFO 5307 --- [           main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager

瀏覽 http://localhost:8080/demo1/test 時,會出現 ## Please sign in 的登入畫面

這時候填寫

username: user
password: 06ee988c-9983-4187-b040-5bfadde83893

就可以打開 index 頁面

可在 application.yml 修改預設的帳號密碼

spring:
  security:
    user:
      name: root
      password: password

自訂安全機制

可註冊 SecurityFilterChain 與 UserDetailsService 類別,實作自訂安全機制

SecurityConfig.java

package tw.com.test.demo1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests((authorize) -> {
            authorize.requestMatchers("/test/**").hasRole("TEST").requestMatchers("/**").permitAll();
        }).logout((logout) -> logout.logoutUrl("/")).formLogin(withDefaults()).build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("test").password("{noop}test").roles("ADMIN", "TEST").build());
        manager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN").build());
        return manager;
    }

}

REST service

實作 REST service 有兩種方式

  • RestTemplate (servlet)

  • WebClient (reactive)

RestTemplate (servlet)

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

自動設定類別 RestTemplateAutoConfiguration

註冊在 org.springframework.boot.autoconfigure.AutoConfiguration.imports

ResTemplate 並沒有任何預設的註冊

必須要先註冊一個 RestTemplate instance,可透過 restTemplateBuilder 產生

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RestTemplate defaultRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .build();
    }
}

新增一個 controller 測試

package tw.com.test.demo1.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
import tw.com.test.demo1.bean.User;

@Slf4j
@RestController
@RequiredArgsConstructor
public class CallRestController {

    public static final String GET_USERINFO_URL = "http://localhost:8080/demo1/user/json/{uid}";
    private final RestTemplate restTemplate;

    @GetMapping("/restTemplate/{uid}")
    public User restTemplate(@PathVariable("uid") String uid) {
        return this.restTemplate.getForObject(GET_USERINFO_URL, User.class, uid);
    }

}

用 postman 測試 GET http://localhost:8080/demo1/restTemplate/10000

回傳結果

{
    "id": 10000,
    "age": 18,
    "username": "test1"
}

自訂 RestTemplate,加上 timeout, basic authentication

    @Bean
    public RestTemplate defaultRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .basicAuthentication("test", "test")
                .build();
    }

也可以直接實作 RestTemplateCustomizer

package tw.com.test.demo1.handler;

import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;


@Slf4j
@Component
public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {

    @Override
    public void customize(RestTemplate restTemplate) {
        HttpRoutePlanner routePlanner = new CustomRoutePlanner(new HttpHost("proxy.test.com"));
        RequestConfig requestConfig = RequestConfig.custom().build();
        HttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setRoutePlanner(routePlanner).build();
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
    }

    static class CustomRoutePlanner extends DefaultProxyRoutePlanner {

        CustomRoutePlanner(HttpHost proxy) {
            super(proxy);
        }

        @Override
        protected HttpHost determineProxy(HttpHost target, HttpContext context) throws HttpException {
            log.info("hostName is {}", target.getHostName());
            if ("localhost".equals(target.getHostName())) {
                return null;
            }
            return super.determineProxy(target, context);
        }

    }

}

有用到 http client 5

        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
        </dependency>

也可以實作最底層的 HttpAccessor


WebClient (reactive)

WebClient 是使用 speign WebFlux 的 http client

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

User.java

package tw.com.test.demo3.bean;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private Long id;
    private String username;
    private Integer age;
}

WebConfig.java

package tw.com.test.demo3.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

@Configuration
public class WebConfig {

    @Bean
    public WebClient webClient(WebClient.Builder webClientBuilder) {
        return webClientBuilder.build();
    }

}

CallRestController.java

package tw.com.test.demo3.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import tw.com.test.demo3.bean.User;

@Slf4j
@RestController
@RequiredArgsConstructor
public class CallRestController {

    public static final String GET_USERINFO_URL = "http://localhost:8080/user/json/{uid}";

    private final WebClient webClient;

    @GetMapping("/webClient/{uid}")
    public Mono<User> webClient(@PathVariable("uid") String uid) {
        return this.webClient.get().uri(GET_USERINFO_URL, uid).retrieve().bodyToMono(User.class);
    }

}

這是要呼叫的 rest url

package tw.com.test.demo3.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import tw.com.test.demo3.bean.User;

@Slf4j
@RestController
@Validated
public class ResponseBodyController {

    @CrossOrigin
    @GetMapping(value = "/user/json/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public User getJsonUserInfo(@PathVariable("userId") String userId) {
        User user = new User((long)1000, "test", 18);
        user.setId(Long.valueOf(userId));
        log.info("user info: {}", user);
        return user;
    }

}

以 GET method 呼叫 http://localhost:8080/webClient/10000

就能取得結果

{
    "id": 10000,
    "username": "test",
    "age": 18
}

自訂 WebClient

修改 WebConfig.java,改用 reactor netty 的 ReactorClientHttpConnector

package tw.com.test.demo3.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

@Configuration
public class WebConfig {

//    @Bean
//    public WebClient webClient(WebClient.Builder webClientBuilder) {
//        return webClientBuilder.build();
//    }

    @Bean
    public WebClient webClient(WebClient.Builder webClientBuilder) {
        HttpClient httpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3).doOnConnected(conn -> {
            conn.addHandlerLast(new ReadTimeoutHandler(3000));
            conn.addHandlerLast(new WriteTimeoutHandler(3000));
        });
        ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
        return webClientBuilder.clientConnector(connector).build();
    }

}

可改用 ClientHttpConnector 的其他實作的客戶端,目前 spring boot 支援

  • Reactor netty (default)

  • Jetty RS Client

  • Apache HttpClient

  • JDK HttpClient