This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

1 - Mapper

Core Domain
<dependency>
  <groupId>tw.com.softleader.data.jakarta</groupId>
  <artifactId>specification-mapper</artifactId>
  <version>${specification-mapper.version}</version>
</dependency>

specification-mapper 是一套 Specifications 的產生器, 它讀取了 Object 中的欄位, 配合欄位上 Annotation 的定義, 來動態的建立查詢條件!

另外 specification-mapper-starter 提供了 Spring Boot 的整合, 讓你可以零配置的在 Spring apps 中使用, 使用 Spring boot 的應用程式可以參考看看!

Getting Started

我們需要的是建立 SpecMapper 實例, 這是最重要的 Class 也是所有 Spec 操作的 API 入口:

var mapepr = SpecMapper.builder().build();

接著我們定義封裝查詢條件的物件, 這是一個 POJO 即可, 如:

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
}

這樣我們就可以做 Specification 的轉換了, 得到 Specification 後就可以依照原本的方式去資料庫查詢, 例如透過 Spring Data JPA 的 repository:

var criteria = new CustomerCriteria();
criteria.setFirstname("Hello")

var mapper = SpecMapper.builder().build();
var specification = mapper.toSpec(criteria);

customerRepository.findAll(specification);

執行 SQL 將會是:

... where x.firstname like '%Hello%'

Skipping Strategy

在 POJO 中的欄位, 只要符合以下任一條件, 在轉換的過程中都將會忽略:

  • 沒有掛任何 Spec Annotation
  • 值為 null
  • 若 Type 為 Iterable 且值為 empty
  • 若 Type 為 Optional 且值為 empty
  • 若 Type 為 CharSequence 且長度為 0
  • 若 Type 為 Array 且長度為 0
  • 若 Type 為 Map 且值為 empty

例如, 將以下 POJO 建構後, 不 set 任何值就直接轉換成 Specification 及查詢

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  String lastname = "Hello";
 
  @Spec
  String nickname = "";
     
  @Spec(GreaterThat.class)
  Optional<Integer> age = Optional.empty();
  
  @Spec(In.class)
  Collection<String> addresses = Arrays.asList();
}

var mapper = SpecMapper.builder().build();
customerRepository.findAll(mapper.toSpec(new CustomerCriteria()));

以上執行的 SQL 將不會有任何過濾條件!

如果你有使用 Builder Pattern, (e.g. Lombok’s @Builder), 請特別注意 Builder 的 Default Value!

若你想要客製化跳脫邏輯, 可以實作 SkippingStrategy, 並在建構 SpecMapper 時傳入:

var mapper = SpecMapper.builder()
      .defaultResolvers()
      .skippingStrategy(fieldValue -> {
        // 判斷是否要跳 field value, 回傳 boolean
      })
      .build();

Simple Specifications

你可以在 Field 使用 @Spec 來定義 Specification 的實作, 預設是 Equals:

@Spec // 同等於 @Spec(Equals.class)
String firstname;

對應的 entity path 預設會使用 field name, 你也可以設定 @Spec#path 來改變

@Spec(path = "...") // 有定義則優先使用
String firstname; // 預設使用欄位名稱

Built-in Simple @Spec

以下是內建的 @Spec 的類型清單:

SpecSupported field typeSampleJPQL snippet
EqualsAny@Spec(Equals.class) String firstname;... where x.firstname = ?
NotEqualsAny@Spec(NotEquals.class) String firstname;... where x.firstname <> ?
BetweenIterable of Comparable
(Expected exact 2 elements in Iterable)
@Spec(Between.class) List<Integer> age;... where x.age between ? and ?
LessThanComparable@Spec(LessThan.class) Integer age;... where x.age < ?
LessThanEqualComparable@Spec(LessThanEqual.class) Integer age;... where x.age <= ?
GreaterThanComparable@Spec(GreaterThan.class) Integer age;... where x.age > ?
GreaterThanEqualComparable@Spec(GreaterThanEqual.class) Integer age;... where x.age >= ?
AfterComparable@Spec(After.class) LocalDate startDate;... where x.startDate > ?
BeforeComparable@Spec(Before.class) LocalDate startDate;... where x.startDate < ?
IsNullBoolean@Spec(IsNull.class) Boolean age;... where x.age is null (if true)
... where x.age not null (if false)
NotNullBoolean@Spec(NotNull .class) Boolean age;... where x.age not null (if true)
... where x.age is null (if false)
LikeString@Spec(Like.class) String firstname;... where x.firstname like %?%
NotLikeString@Spec(NotLike.class) String firstname;... where x.firstname not like %?%
StartingWithString@Spec(StartingWith.class) String firstname;... where x.firstname like ?%
EndingWithString@Spec(EndingWith.class) String firstname;... where x.firstname like %?
InIterable of Any@Spec(In.class) Set<String> firstname;... where x.firstname in (?, ?, ...)
NotInIterable of Any@Spec(NotIn.class) Set<String> firstname;... where x.firstname not in (?, ?, ...)
TrueBoolean@Spec(True.class) Boolean active;... where x.active = true (if true)
... where x.active = false (if false)
FalseBoolean@Spec(False.class) Boolean active;... where x.active = false (if true)
... where x.active = true (if false)
HasLengthBoolean@Spec(HasLength.class) Boolean firstname;... where x.firstname is not null and character_length(x.firstname)>0 (if true)
... where not(x.firstname is not null and character_length(x.firstname)>0) (if false)
HasTextBoolean@Spec(HasText.class) Boolean firstname;... where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0 (if true)
... where not(where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0) (if false)

為了方便已經熟悉 Spring Data JPA 的人使用, 以上名稱都是儘量跟著 Query Methods 一樣

Negates the @Spec

你可以使用 @Spec#not 來判斷反向條件, 預設是 false, 設定成 true 就會將結果做反向轉換.

例如, 我想要用 Between不在區間內的資料, 則範例如下:

@Spec(value = Between.class, not = true)
Collection<Integer> age;

執行的 SQL 將會是:

... where x.age not between ? and ?

Extending Simple @Spec

@Spec 是可以很容易擴充的, 只要實作了 SimpleSpecification<T> 並提供規定的 Constructor, 這些 class 就可以被定義在 @Spec

例如, 我有個 Customer Entity, 有以下欄位:

  • firstname (String) - 人名, 可重複
  • createdTime (LocalDateTime) - 建立時間, 不可重複

我希望可以找出每個人名中, 建立時間為最新的那筆資料! 且我打算撰寫一個 subquery 來完成這需求, 完整的範例如下:

首先我們實作 SimpleSpecification<T>, 並提供規定的 Constructor:

public class MaxCustomerCreatedTime extends SimpleSpecification<Customer> {

  // 這是規定必須提供的建構值, 修飾詞不限定: public, protected, default, private 都支援
  protected MaxCustomerCreatedTime(Context context, String path, Object value) {
    super(context, path, value);
  }

  @Override
  public Predicate toPredicate(Root<Customer> root,
    CriteriaQuery<?> query,
    CriteriaBuilder builder) {
    // 以下提供 subquery 的實作, 僅供參考
    var subquery = query.subquery(LocalDateTime.class);
    var subroot = subquery.from(Customer.class);
    subquery.select(
      builder.greatest(subroot.get("createdTime").as(LocalDateTime.class))
    ).where(builder.equal(root.get((String) value), subroot.get((String) value)));
    return builder.equal(root.get("createdTime"), subquery);
  }
}

上面完成的 MaxCustomerCreatedTime 就可以被應用在 @Spec 中了, 接著我們定義 POJO 及進行 Specification 的轉換:

@Data
public class CustomerCriteria {

  @Spec(MaxCustomerCreatedTime.class)
  String maxBy;
}

var criteria = new CustomerCriteria();
criteria.setMaxBy("firstname");

var spec = mapper.toSpec(criteria, Customer.class);
repository.findAll(spec);

執行的 SQL 將會是:

... where customer0_.created_time=(
  select max(customer1_.created_time) from customer customer1_ 
  where customer0_.firstname=customer1_.firstname
)

Combining Specs

你可以在 class 層級上使用 @And@Or 來組合一個物件中的多個 Specification, 組合的預設是 @And.

例如我想要改成 @Or, 程式碼範例如下:

@Or // 若沒定義預設就是 @And
@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  @Spec(Like.class)
  String lastname;
}

執行的 SQL 將會是:

... where x.firstname like %?% or x.lastname like %?% 

Specify Combining Type on Field

你也可以將 @And@Or 註記在欄位上來控制單獨一個欄位要怎麼跟其他欄位組合. 舉個例子如下:

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  @Spec(Like.class)
  String lastname;

  @Or
  @Spec(value = After.class, not = true)
  LocalDate birthday;
}

執行的 SQL 將會是:

... where (x.firstname like ?) and (x.lastname like ?) or x.birthday<=?

特別注意, 欄位是依照宣告的順序組合的, 而 SQL 也是有運算子優先順序的, 需注意兩者的配合的結果是否符合你的期望

例如, 將上面的例子調整欄位順序:

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  @Or
  @Spec(value = After.class, not = true)
  LocalDate birthday;
  
  @Spec(Like.class)
  String lastname;
}

執行的 SQL 將會是:

... where (x.firstname like ? or x.birthday<=?) and (x.lastname like ?)

Nested Specs

你可以在 Field 上使用 @NestedSpec 來告知 SpecMapper 要往下一層物件 (Nested Object) 去組合 Specification, 這是沒有層級限制的, 可以一直往下找!

例如我有一個共用的 AddressCriteria POJO, 我就可以將它掛載到其他的 POJO 中, 程式碼範例如下:

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  @NestedSpec
  AddressCriteria address;
}

@Or
@Data
public class AddressCriteria {

  @Spec
  String county;
  
  @Spec
  String city;
}

執行的 SQL 將會是:

... where x.firstname like %?% and ( x.county=? or x.city=? )

Specify Combining Type on Nested Object

你也可以在 Nested Object 的欄位上宣告 @And@Or 來控制結果要怎麼跟其他的欄位組合, 詳細的說明請參考 Specify Combining Type on Field.

舉個例子如下:

@Data
public class CustomerCriteria {

  @Spec(Like.class)
  String firstname;
  
  @Or
  @NestedSpec
  AddressCriteria address;
}

@Data
public class AddressCriteria {

  @Spec
  String county;
  
  @Spec
  String city;
}

執行的 SQL 將會是:

... where (x.firstname like ?) or x.county=? and x.city=?

Join

你可以在 Field 上使用 @Join 來過濾關聯 entity, 這些 entity 之間需要都先定義好關係, 例如:

@Entity
class Customer {

  @OneToMany(cascade = ALL, fetch = LAZY)
  @JoinColumn(name = "order_id")
  private Collection<Order> orders;
}

@Entity
class Order {

  private String itemName;
}

如果你想要查詢買了指定東西的客戶, 則可以定義 POJO 如下:

@Data
public class CustomerOrderCriteria {

  @Join(path = "orders", alias = "o")
  @Spec(path = "o.itemName", value = In.class)
  Collection<String> items;
}

執行的 SQL 將會是:

select distinc ... from customer customer0_ 
inner join orders orders1_ on customer0_.id=orders1_.order_id 
where orders1_.item_name in (? , ?)

為了比較符合大部分的使用情境, 預設的 Join type 是 INNER, 也會將結果排除重複 (distinct), 你可以設定 @Join#joinType@Join#distinct 來改變, 如:

@Join(joinType = JoinType.RIGHT, distinct = false)

Multi Level Joins

你可以使用 @Joins 來定義多層級的 Join, 例如:

@Entity
class Customer {

  @OneToMany(cascade = ALL, fetch = LAZY)
  @JoinColumn(name = "order_id")
  private Set<Order> orders;
}

@Entity
class Order {
    
  @ManyToMany(cascade = ALL, fetch = LAZY)
  private Set<Tag> tags;
}

@Entity
class Tag {

  private String name;
}

如果你想要查詢買了指定所屬類別的東西的客戶, 則可以定義 POJO 如下:

@Data
class CustomerOrderTagCriteria {

  @Joins({
    @Join(path = "orders", alias = "o"),
    @Join(path = "o.tags", alias = "t")
  })
  @Spec(path = "t.name", value = In.class)
  Collection<String> tags;
}

執行的 SQL 將會是:

select distinct ... from customer customer0_ 
inner join orders orders1_ on customer0_.id=orders1_.order_id 
inner join orders_tags tags2_ on orders1_.id=tags2_.order_id 
inner join tag tag3_ on tags2_.tags_id=tag3_.id 
where tag3_.name in (?)

特別注意, Annotation 的處理是有順序性的, 因此必須依照 Join 的順序去定義 @Joins

例如依照上面的情境, 下列的定義順序是錯誤的:

@Data
class CustomerOrderTagCriteria {

  @Joins({
    @Join(path = "o.tags", alias = "t"), // "o" alias will be not exist during processing this @Join
    @Join(path = "orders", alias = "o")
  })
  @Spec(path = "t.name", value = In.class)
  Collection<String> tagNames;
}

Join Fetch

你可以在 class 層級上使用 @JoinFetch 可以一次撈出所有 Lazy 的關聯資料, 例如:

@Entity
class Customer {

  @OneToMany(fetch = LAZY, cascade = ALL)
  @JoinColumn(name = "order_id")
  private Collection<Order> orders;
}

@Entity
class Order {
  
  private String itemName;
}

如果你想在取得 Customer 時就順便 Join 出 Order, 則:

@JoinFetch(paths = "orders")
@Data
class CustomerOrderCriteria {

  @Spec
  String name;
}

執行的 SQL 將會是:

select distinct 
  customer0_.* ...,
  orders1_.* ...
from customer customer0_ 
inner outer join orders orders1_ on customer0_.id=orders1_.order_id 
where customer0_.name=?

為了比較符合大部分的使用情境, 預設的 Join type 是 INNER, 也會將結果排除重複 (distinct), 你可以設定 @FetchJoin#joinType@FetchJoin#distinct 來改變, 如:

@FetchJoin(joinType = JoinType.RIGHT, distinct = false)

Multi Level Fetch Joins

你可以使用 @FetchJoins 來定義多層級的 Fetch Join, 例如:

@Entity
class Customer {

  @OneToMany(cascade = ALL, fetch = LAZY)
  @JoinColumn(name = "order_id")
  private Set<Order> orders;
}

@Entity
class Order {
    
  @ManyToMany(cascade = ALL, fetch = LAZY)
  private Set<Tag> tags;
}

@Entity
class Tag {

  private String name;
}

如果你想在取得 Customer 時就順便 Join 出 Order 及 Tag, 則:

@JoinFetches({
  @JoinFetch(paths = "orders"),
  @JoinFetch(paths = "orders.tags")
})
@Data
class CustomerOrderTagCriteria {

  @Spec
  String name;
}

執行的 SQL 將會是:

select distinct 
  customer0_.* ...,
  orders1_.* ...,
  tags3_.* ...
from customer customer0_ 
left outer join orders orders1_ on customer0_.id=orders1_.order_id 
inner join orders orders2_ on customer0_.id=orders2_.order_id 
left outer join orders_tags tags3_ on orders2_.id=tags3_.order_id 
left outer join tag tag4_ on tags3_.tags_id=tag4_.id 
where customer0_.name=?

Customize Spec Annotation

延續 Extending Simple @Spec 章節範例, 進階一點現在我們希望可以將 Entity Class 設計成可以配置, 這樣才能在 Customer 以外的 Entity 都可以使用!

要完成這需求我們需要在 Annotation 中定義更多參數, 因此 Simple @Spec 不適用了, 我們需要的是定義新的 Annotation, 完整的程式碼如下:

首先我們定義 Annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface MaxCreatedTime {

  Class<?> from();
}

接著我們要撰寫負責處理 @MaxCreatedTime 的邏輯, 透過實作 SpecificationResolver 來擴充:

public class MaxCreatedTimeSpecificationResolver implements SpecificationResolver {

  @Override
  public boolean supports(Databind databind) { 
    // 這邊告訴 SpecMapper 什麼時候要使用此 resolver
    return databind.getField().isAnnotationPresent(MaxCreatedTime.class);
  }

  @Override
  public Specification<Object> buildSpecification(Context context, Databind databind) {
    var def = databind.getField().getAnnotation(MaxCreatedTime.class);
    return databind.getFieldValue()
        .map(value -> subquery(def.from(), value.toString()))
        .orElse(null);
  }

  Specification<Object> subquery(Class<?> entityClass, String by) {
    // 以下提供 subquery 的實作, 僅供參考
    return (root, query, builder) -> {
      var subquery = query.subquery(LocalDateTime.class);
      var subroot = subquery.from(entityClass);
      subquery.select(
        builder.greatest(subroot.get("createdTime").as(LocalDateTime.class))
      ).where(builder.equal(root.get(by), subroot.get(by)));
      return builder.equal(root.get("createdTime"), subquery);
    };
  }
}

接著我們在 SpecMapper 建構時加入此 resolver:

var mapper = SpecMapper.builder()
      .defaultResolvers()
      .resolver(new MaxCreatedTimeSpecificationResolver())
      .build();

最後我們定義 POJO 及進行 Specification 的轉換:

@Data
public class CustomerCriteria {

  @MaxCreatedTime(from = Customer.class)
  String maxBy;
}

var criteria = new CustomerCriteria();
criteria.setMaxBy("firstname");

var spec = mapper.toSpec(criteria, Customer.class);
repository.findAll(spec);

執行的 SQL 將會是:

... where customer0_.created_time=(
  select max(customer1_.created_time) from customer customer1_ 
  where customer0_.firstname=customer1_.firstname
)

Logging

tw.com.softleader.data.jpa.spec.SpecMapper 設定為 debug, 會在物件轉換成 Spec 的過程中印出更多資訊, 可以有效的幫助查找問題, 如:

DEBUG 20297 --- [           main] t.c.softleader.data.jpa.spec.SpecMapper  : --- Spec AST ---
+-[CustomerCriteria]: my.package.CustomerCriteria
|  +-[CustomerCriteria.firstname]: @Spec(value=Equals, path=, not=false) -> Equals[path=name, value=matt]
|  +-[CustomerCriteria.address]: my.package.AddressCriteria (NestedSpecificationResolver)
|  |  +-[AddressCriteria.county]: @Spec(value=Equals, path=, not=false) -> null
|  |  +-[AddressCriteria.city]: @Spec(value=Equals, path=, not=false) -> Equals[path=name, value=Taipei]
|  \-[CustomerCriteria.address]: Conjunction[specs=[Equals[path=city, value=Taipei]]]
\-[CustomerCriteria]: Conjunction[specs=[Equals[path=name, value=matt], Conjunction[specs=[Equals[path=city, value=Taipei]]]]]

如果你喜歡根據轉換的物件來控制和設定 Logging, 我們提供了另一種策略, 通過設定 ASTWriterFactory, 可以改成使用目標物件的 Logger 來輸出:

var mapper = SpecMapper.builder()
      .defaultResolvers()
      .astWriterFactory(ASTWriterFactory.impersonation())
      .build();

Limitation

SpecMapper 在找 POJO 欄位時, 只會找當前 Class 的 Local Field, 而不去往上找 Hierarchy Classes 的 Field, 如果你共用的欄位想要用在多個 POJO, 請考慮使用 Nested Specs 方式 mapper/README.zh-tw.md

2 - Starter

Spring Starter
<dependency>
  <groupId>tw.com.softleader.data.jakarta</groupId>
  <artifactId>specification-mapper-starter</artifactId>
  <version>${specification-mapper.version}</version>
</dependency>

specification-mapper-starter 整合了 specification-mapperSpring Data JPA, 並提供了 Query by Spec 的查詢方式等

Query by Spec (QBS) 是一個 user-friendly 的查詢方式, 可以動態的建立查詢條件 (Specifications), 透過 QBS interface 就可以執行查詢語句!

Getting Started

只要在 pom.xml 中加入 dependency, 此 Starter 在 Spring Boot 啟動過程就會自動的配置一切, 讓你可以零配置的就開始使用, 包含了:

自動配置預設是啟用的, 你可以透過 properties 中的 spec.mapper.enabled 控制, 如要關閉則:

spec:
  mapper:
    enabled: false

Query by Spec

Query by Spec (QBS) 提供了 QueryBySpecExecutor<T> 包含了許多查詢方法:

public interface QueryBySpecExecutor<T> {

  List<T> findBySpec(Object spec);
  
  List<T> findBySpec(Object spec, Sort sort);
  
  Page<T> findBySpec(Object spec, Pageable pageable);
  
  // … more functionality omitted.
}

只要在原本的 repository interface 中去繼承 QueryBySpecExecutor<T> 就可以直接使用了:

public interface PersonRepository 
  extends JpaRepository<Person, Long>, QueryBySpecExecutor<Person> {
  ...
}

@Service
public class PersonService {

  @Autowired PersonRepository personRepository;

  public List<Person> findPeople(PersonCriteria criteria) {
    return personRepository.findBySpec(criteria);
  }
}

Customize the QBS Base Repository

在配置的過程中, QBS 會自動配置 Spring Data JPA 的 Base Repository, 預設的實作為 QueryBySpecExecutorImpl

由於 Java 只能單一繼承, 為了應用程式可以保留原有的 Parent Base Repository, QBS 還多提供了 QueryBySpecExecutorAdapter 擴展點

你的應用程式可以視情況選擇繼承 QueryBySpecExecutorImpl 或實作 QueryBySpecExecutorAdapter 去客製化 Base Repository, 如:

class MyRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>
  implements QueryBySpecExecutorAdapter<T> {

  @Setter
  @Getter
  private SpecMapper specMapper;

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Override
  public Class<T> getDomainClass() {
    return super.getDomainClass();
  }
  
  @Transactional
  public <S extends T> S mySave(S entity) {
    // implementation goes here
  }
}

並且透過 properties 中的 spec.mapper.repository-base-class 設定成自定義的 base repository 的 fulll package name, 如:

spec:
  mapper:
    repository-base-class: com.acme.example.MyRepositoryImpl

Default SpecMapper

此 Starter 會在 App 啟動的過程中自動的配置一個 Default SpecMapper 並註冊到 Spring @Bean 中, 你可以透過 Autowired 的方式跟 Spring 取得.

例如, 我想要在轉換成 Specification 後, 先做一些加強再去查詢, 則範例如下:

class PersonService {

  @Autowired SpecMapper specMapper;
  @Autowired PersonRepository personRepository;

  List<Person> getPersonByCriteria(PersonCriteria criteria) {
    var spec = specMapper.toSpec(criteria);
    
    // Perform additional operations on the spec, ex:
    // spec = spec.and((root, query, criteriaBuilder) ->  {
    //     ...
    // });
    
    return personRepository.findAll(spec);
  }
}

Customize SpecificationResolver

只要將你自定義的 SpecificationResolver 註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測並加入到 Default SpecMapper 中!

例如, 我想要增加自定義的 Spec Annotation, 配置範例如下:

@Configuration
class MyConfig {

  @Bean
  SpecificationResolver myResolver() {
    return ...
  }
}

如果你的 SpecificationResolver 需要用到 SpecMapper 本身, 則你可以包裝成 SpecificationResolverCodecBuilder, 在建構 resolver 時就會把 SpecCodec, 即 SpecMapper 的 interface, 傳進去, 例如:

@Configuration
class MyConfig {

  @Bean
  SpecificationResolverCodecBuilder myResolver() {
    return MySpecificationResolver::new;
  }
}

class MySpecificationResolver implements SpecificationResolver {
  
  private final SpecCodec codec;
  
  MySpecificationResolver(SpecCodec codec) {
    // Keep the SpecCodec around to used.
    this.codec = codec;
  }
  
  // implementation goes here
}

Customize SkippingStrategy

只要將你自定義的 SkippingStrategy 註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測並加入到 Default SpecMapper 中!

配置範例如下:

@Configuration
class MyConfig {

  @Bean
  SkippingStrategy mySkippingStrategy() {
    return ...
  }
}

Customize ASTWriterFactory

透過 properties 中的 spec.mapper.impersonate-logger, 可以設定 Logging 過程中, 是否要偽裝成實際處理的 object logger, 預設是關閉的, 若要開啟範例如下:

spec:
  mapper:
    # 是否要偽裝成實際處理的 object logger, 預設關閉
    impersonate-logger: true

若你需要完整的客製化, 只要將你自定義的 ASTWriterFactory 註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測並加入到 Default SpecMapper 中!

配置範例如下:

@Configuration
class MyConfig {

  @Bean
  ASTWriterFactory myASTWriterFactory() {
    return ...
  }
}

Customize Default SpecMapper

當然, 你也可以完全的客製化 SpecMapper, 只要將你的 SpecMapper 註冊成 Spring @Bean, App 啟動的過程中就會略過 Default SpecMapper 的配置而優先採用的你所註冊的那個!

配置範例如下:

@Configuration
class MyConfig {

  @Bean
  SpecMapper mySpecMapper() {
    return SpecMapper.builder()
      . ...
      .build();
  }
}