-
Json body parameters no longer throws when:
- Content-type is omitted
- Content-type is any other than application/json
This means that empty request body is accepted when json paths are present in specifications.
- Fixed a bug related to specifications that always returned all available values. Fixed specs:
InThePast
InTheFuture
- Migrated project to spring boot 3.0 and java 17
- Spring boot 3.0 is based on Hibernate version 6.X because in this version of hibernate all query results are distinct by default. This shouldn't affect most projects, but please be extra careful if you've ever used a spec with the
distinct=false
attribute.
- Spring boot 3.0 is based on Hibernate version 6.X because in this version of hibernate all query results are distinct by default. This shouldn't affect most projects, but please be extra careful if you've ever used a spec with the
- Added support for spring native-image.
- Specification-arg-resolver can be used in GraalVM native images, but it requires several additional configuration steps. This is due to the fact that this library relies on Java reflection heavily. Please see README_native_image.md for the details
- Modified Springdoc-openapi dependency to be compatible with spring boot 3.0
- Refactored
IsMember
,IsNotMember
specifications - they no longer use reflection explicitly. - changed default join type in
@Join
fromINNER
toLEFT
. I.e. if you have been using@Join
with default join type:Then, if you want to keep INNER join behaviour, you need to make it explicit (otherwise it will be LEFT):@Join(path="addresses", alias="a")
@Join(path="addresses", alias="a", type=JoinType.INNER)
- Changed join lazy evaluation:
- Inner joins are now evaluated even if there is no filtering applied on the joined part due to a missing HTTP param (as inner join may narrow down query results)
- For non-distinct queries, all joins are now evaluated eagerly (even if there is no filtering applied on the joined part). Reminder for Hibernate users: from Hibernate 6 onwards, all queries are distinct anyway
- in all other situations, left and right joins are not evaluated unless there is filtering on the joined part (and the corresponding HTTP param is present)
- Removed all deprecated items, including:
DateAfter
,DateAfterInclusive
,DateBefore
,DateBeforeInclusive
- useGreaterThan
,GreaterThanOrEqual
,LessThan
orLessThanOrEqual
DateBetween
- useBetween
EqualEnum
- useEqual
@Joins
container annotation - use repeated@Join
and@JoinFetch
-
Json body parameters no longer throws when:
- Content-type is omitted
- Content-type is any other than application/json
This means that empty request body is accepted when json paths are present in specifications.
- Fixed a bug related to specifications that always returned all available values. Fixed specs:
InThePast
InTheFuture
IsNull
specification is no longer deprecated- Introduced
IsNotNull
specification.
- Introduced converter for
char
primitive andCharacter
class - Introduced new specifications:
isEmpty
,isNotEmpty
- these specifications filter out elements that have empty (not empty) collection of elements, that is defined underpath
in@Spec
annotation.Empty
- this specification filters for collections usingis empty
oris not empty
, depending on the value of the parameter passed in (e.g.where customer.orders is empty
).NotEmpty
- it is a negation forEmpty
specification.isTrue
,isFalse
- these specifications filter withtrue
/false
value of particular field defined underpath
in@Spec
annotation.True
- this specification filters usingtrue
orfalse
for a boolean type field, depending on the value of the parameter passed in.False
- it is a negation forTrue
specification.isMember
,isNotMember
- checks if the value passed as HTTP parameter is a member of a collection attribute of an entity (defined underpath
in@Spec
annotation).
- Introduced
EqualDay
specification which allows finding all records within particular date (day), ignoring time. - Added ability to set custom
Locale
during resolver registration:This matters for case-insensitive specifications (@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new SpecificationArgumentResolver(new Locale("pl", "PL"))); // pl_PL will be used as the default locale }
EqualIgnoreCase
,NotEqualIgnoreCase
,LikeIgnoreCase
,StartingWithIgnoreCase
andEndingWithIgnoreCase
) which used system default locale in previous versions of the library. If locale is not provided, then system default will be used (exactly as in the previous version). - Added ability to set custom
Locale
in@Spec.config
(this overrides the global default mentioned above):@Spec(path = "name", spec = EqualIgnoreCase.class, config = "tr_TR")
- Introduced new case-insensitive specification
NotLikeIgnoreCase
that works in similar way asLikeIgnoreCase
but is its negation. - introduced
missingPathVarPolicy
to@Spec
annotation with available values:IGNORE
andEXCEPTION
(default). New policy is intended to configure behaviour on missing path variable.- for more details please check out section
Support for multiple paths with path variables
inREADME.md
.
- for more details please check out section
- additional Javadocs
- updated spring-boot-dependencies to 2.7.7
- fixed potential issue with detecting non-empty HTTP headers
- fixed redundant proxy creation for multi-spec specifications when expected type is not a spec-interface
- added support for using datetime formats without time (e.g.
yyyy-MM-dd
) for types that contain time (LocalDateTime
,Timestamp
,Instant
,OffsetDateTime
). Missing time values are filled with zeros, e.g. when sending2022-12-14
asLocalDateTime
parameter, the conversion will result in2022-12-14 00:00
. - introduced
InTheFuture
specification, that supports date-type paths - introduced
InThePast
specification, that supports date-type paths - added exception messages for invalid parameter array size in specifications that missed one
- Added support for
content-type
header containing additional directives likeencoding=UTF-8
/charset=UTF-8
. Previously, onlyapplication/json
was accepted ascontent-type
for request body filters.
- added support for
jsonPaths
during generation of swagger documentation. - fixed bugs related to swagger support:
- fixed marking
headers
andpathVars
parameters as required/non-required. From now allpathVars
are marked as required andheaders
can be marked as required depending on controller method configuration. - fixed duplicated parameters when the same parameter was defined in spec and controller method (e.g. when we defined
firstName
parameter in our@Spec
and also in@RequestParam("firstName")
).
- fixed marking
- added
OnTypeMismatch.IGNORE
which ignores specification containing mismatched parameter (exceptspec = In.class
- in this specification only mismatched parameter values are ignored, but other ones which are valid are used to build a Specification).- For example, for the following endpoint:
@RequestMapping(value = "/customers", params = { "id" }) @ResponseBody public Object findById( @Spec(path = "id", params = "id", spec = Equal.class, onTypeMismatch = IGNORE) Specification<Customer> spec) { return customerRepo.findAll(spec); }
- For request with mismatched
id
param (e.g.?id=invalidId
) the whole specification will be ignored and all records from the database (without filtering) will be returned. - But for the following endpoint with
In.class
specification type:@RequestMapping(value = "/customers", params = { "id_in" }) @ResponseBody public Object findByIdIn( @Spec(path = "id", params = "id_in", spec = In.class, paramSeparator = ",", onTypeMismatch = IGNORE) Specification<Customer> spec) { return customerRepo.findAll(spec); }
- For request with params
?id_in=1,2,invalidId
- only valid params will be taken into consideration (invalid params (not the whole specification) will be ignored) - For request with only invalid params
id_in=invalidId1,invalidId2
- an empty result will be returned as there are only invalid parameters (which are ignored).
- For example, for the following endpoint:
- added Json request body support. This requires adding
gson
dependency to your project and has some limitations -- see json section of README.md for more details.
- Fixed bug in
SpecificationBuilder
that was creating doubled query conditions. - Changed approach for resolving path variables when processing request.
- From now on, the controllers with global prefixes (configured using
org.springframework.web.servlet.config.annotation.PathMatchConfigurer
) should be properly handled:- For example, apps with following configuration are now supported:
Below spec will be properly resolved for request URI:
@Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("/api/{tenantId}", HandlerTypePredicate.forAnnotation(RestController.class)); }
/api/123/findCustomers?firstName=John
@RestController public static class TestController { @GetMapping("/findCustomers") public List<Customer> findCustomersByFirstName(@And(value = { @Spec(path = "tenantId", pathVar = "tenantId", spec = Equal.class), @Spec(path = "firstName" param = "firstName", spec = Equal.class) }) Specification<Customer> spec) { return customerRepository.findAll(spec); } }
- For example, apps with following configuration are now supported:
- added support for
SpringDoc-OpenAPI
library -- parameters from specification will be shown in generated documentation
-
replaced hibernate java persistence api dependency with java persistence api (
org.hibernate.javax.persistence
->javax.persistence
) -
Added
SpecificationBuilder
that allows creating specification apart from web layer.For example:
- Let's assume the following specification:
@Join(path = "orders", alias = "o") @Spec(paths = "o.itemName", params = "orderItem", spec=Like.class) public interface CustomerByOrdersSpec implements Specification<Customer> { }
- To create specifications outside the web layer, you can use the specification builder as follows:
Specification<Customer> spec = SpecificationBuilder.specification(CustomerByOrdersSpec.class) // good candidate for static import .withParam("orderItem", "Pizza") .build();
- It is recommended to use builder methods that corresponding to the type of argument passed to specification interface, e.g.:
- For:
you should use@Spec(paths = "o.itemName", params = "orderItem", spec=Like.class)
withParam(<argName>, <values...>)
method. Each argument type (param, header, path variable) has its own corresponding builder method:params = <args>
=>withParam(<argName>, <values...>)
, single param argument can provide multiple valuespathVars = <args>
=>withPathVar(<argName>, <value>)
, single pathVar argument can provide single valueheaders = <args>
=>withHeader(<argName>, <value>)
, single header argument can provide single value
The builder exposes a method
withArg(<argName>, <values...>)
which allows defining a fallback value. It is recommended to use it unless you really know what you are doing. - Let's assume the following specification:
- fixed bug with not evaluated join fetches in count queries (e.g. during pagination) -- from now on, join fetches in count queries are either skipped (if they are used solely for initialization of lazy collections) or converted to regular joins (if there is any filtering applied on the fetched part). See issue 138 for more details.
- added conversion support for
Timestamp
- Added strict date format validation for
Date
,Calendar
andTimestamp
inConverter
component.- Let's assume following specification definition:
@Spec(path = "startDate", params = "periodStart", spec = Equal.class, config = "yyyy-MM-dd")
- Previously, the request parameter values was parsed as follows:
2022-11-28-unnecessary-additional-characters
was parsed to2022-11-28
(if the date format was satisfied (checking from left to right) the next additional characters were ignored)28-11-2022
was parsed to invalid date (different from2022-11-28
), order of specific parts of date was not validated.1-1-1
was parsed to invalid date (length of specific parts of date (year, month, day) was not validated)
- From now on strict policy of date format validation is introduced. The Date has to be in specific format and of specific length.
- Previously, the request parameter values was parsed as follows:
- Let's assume following specification definition:
- Fixed the bug with redundant joins
- Added conversion support for
Calendar
-
Added spring cache support for custom specification interfaces. From now on, specifications generated from specification interfaces with the same params are equal and have the same
hashCode
value. -
Added support for join fetch aliases in specification paths.
For example:
@RequestMapping(value = "/customers", params = { "orderedItemName" }) @ResponseBody public Object findCustomersByOrderedItemName( @JoinFetch(paths = "orders", alias = "o") @Spec(path = "o.itemName", params = "orderedItemName", spec = Like.class)) Specification<Customer> spec) { return customerRepository.findAll(spec, Sort.by("id")); }
Please remember that:
- Join fetch path can use only aliases of another fetch joins.
- Join path can use only aliases of another joins.
(see README.md for the details)
- added support for resolving HTTP param name from a SpEL expression (via
@Spec.paramsInSpEL
) - added support for resolving query arguments from HTTP request headers (via
@Spec.headers
)
- supporting JDK17 (previous version threw exceptions on illegal reflection operations)
- fixed pagination support for multi-level joins
- fixed bug which caused invalid query to be created when multiple
@JoinFetch
annotations referenced the same alias
-
Added support for multi-level joins.
It's now possible to define multi-level join where each join can use aliases defined by previous joins (see README.md for the details).
For example:
@RequestMapping(value = "/findCustomersByOrderedItemTag") @PostMapping public Object findCustomersByOrderedItemTag( @Join(path = "orders", alias = "o") @Join(path = "o.tags", alias = "t") @Spec(path = "t.name", params = "tag", spec = Equal.class) Specification<Customer> spec) { return customerRepo.findAll(spec, Sort.by("id")); }
Multi-level join fetch could be defined similarly to multi-level join.
For example:
@RequestMapping(value = "/findCustomers") @PostMapping public Object findAllCustomers( @JoinFetch(paths = "orders", alias = "o") @JoinFetch(paths = "o.tags") Specification<Customer> spec) { return customerRepo.findAll(spec).stream() .map(this::mapToCustomerDto) .collect(toList());
-
Added support for SpEL and property placeholders in
@Spec
attributes:constVal
,defaultVal
.To enable SpEL support:
- Configure
SpecificationArgumentResolver
by passingAbstractApplicationContext
in constructor - Set
Spec
attributevalueInSpEL
value totrue
Configuration example:
@Autowired AbstractApplicationContext applicationContext; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new SpecificationArgumentResolver(applicationContext)); }
Usage example of default value with property placeholder:
@RequestMapping(value = "/customers") @ResponseBody public Object findByLastName( @Spec(path = "id", params="lastName", defaultVal='${search.default-params.lastName}', valueInSpEL = true, spec = Equal.class) Specification<Customer> spec) { return customerRepo.findAll(spec); }
application.properties
search.default-params.lastName=Simpson
Usage example of default value in SpEL:
@RequestMapping(value = "/customers") @ResponseBody public Object findCustomersWhoCameFromTheFuture( @Spec(path = "id", params="birthDate", defaultVal='#{T(java.time.LocalDate).now()}', valueInSpEL = true, spec = GreaterThanOrEqual.class) Specification<Customer> spec) { return customerRepo.findAll(spec); }
- Configure
-
Added support for repeatable
@Join
and@JoinFetch
annotations.@Joins
annotation is now deprecated and it's going to be removed in the future.To specifying multiple different joins, repeated
@Join
annotation should be used:@RequestMapping(value = "/findBy", params = {""}) public void findByBadgeTypeAndOrderItemName( @Join(path = "orders", alias = "o", type = JoinType.LEFT) @Join(path = "badges", alias = "b", type = JoinType.LEFT) @Or({ @Spec(path = "o.itemName", params = "order", spec = Like.class), @Spec(path = "b.badgeType", params = "badge", spec = Equal.class) }) Specification<Customer> spec) { return customerRepository.findAll(spec); }
instead of using annotation container
@Joins
. -
Added support for enum in specs:
EqualIgnoreCase.class
,NotEqualIgnoreCase.class
- Fixed
NullPointerException
for requests with missing params to an endpoint with specs which uses param separator. In previous versionsNullPointerException
had been thrown for requests with missing parameters. Now spec withparamSeparator
attribute is skipped for request with missing params.
- Added
distinct
(default:true
) attribute toJoinFetch
annotation. Attribute determines that query should be distinct or not.
-
Added conversion support for
UUID
,OffsetDatetime
,Instant
-
Added a fallback mechanism to
Converter
which uses converters registered in ConversionService. TheConverter
in case of missing converter for a given type tries to find a required converter in SpringConversionService
, ifConversionService
does not support required conversionIllegalArgumentException
will be thrown. If the required converter is not present inConverter
andConversionService
it could be defined and used as follows:@Configuration @EnableJpaRepositories public class MyConfig implements WebMvcConfigurer { @Autowired ConversionService conversionService; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new SpecificationArgumentResolver(conversionService)); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToAddressConverter()); } public static class StringToAddressConverter implements Converter<String, Address> { @Override public Address convert(String rawAddress) { Address address = new Address(); address.setStreet(rawAddress); return address; } } ... }
-
Added support for path variables with regexp. All patterns supported by spring AntPathMatcher are supported.
For example:
@RequestMapping(value = "/pathVar/customers/{customerId:[0-9]+}") @ResponseBody public Object findById( @Spec(path = "id", pathVars = "customerId", spec = Equal.class) Specification<Customer> spec) { return customerRepo.findAll(spec); }
-
Fixed support for custom interfaces with complex inheritance tree. In previous versions, annotations: @Join, @JoinFetch, @Joins were supported only for the lowest interface in the inheritance tree.
Following example didn't work before fix:
@Join(path= "orders", alias = "o") @Spec(path="o.id", params="orderId", spec=Equal.class) public interface CommonFilter<T> extends Specification<T> { } public interface CustomerFilter extends CommonFilter<Customer> { }
WebRequestQueryContext
has been improved to use an actual root query instance during path evaluation, rather than the one being cached before. This should fix problems with count queries- Made
@JoinFetch
query distinct by default to keep the behavior in line with@Join
.
-
Added support for passing multiple values as a single HTTP parameter. The new
paramSeparator
attribute of@Spec
can be used to define the separator (e.g. comma). For example the following controller method:@RequestMapping(value = "/customers", params = "genderIn") @ResponseBody public Object findCustomersByGender( @Spec(path = "gender", params = "genderIn", paramSeparator = ",", spec = In.class) Specification<Customer> spec) { return customerRepo.findAll(spec); }
will handle
GET http://myhost/customers?gender=MALE,FEMALE
in exactly the same way asGET http://myhost/customers?gender=MALE&gender=FEMALE
- fixed path variable resolving in environments where
HttpServletRequest.getPathInfo()
returnsnull
-
added possibility to define a default value for filtering, as a fallback when HTTP param is not present. For example this controller method:
@RequestMapping("/users") public Object findByRole( @Spec(path="role", spec=Equal.class, defaultVal="USER") Specification<User> spec) { return userRepo.findAll(spec); }
Would handle request such as
GET /users
with the following query:select u from Users u where u.role = 'USER'
. -
added new specifications:
StartingWith
,EndingWith
and their case-insensitive counterparts -
added new specification negations:
NotIn
,NotLike
-
requires Java 8 + intended for Spring Boot 2.x
-
fixed bug with repeated joins
-
optimized joining: joins will not be performed if no filtering is applied on the join path
-
fixed
OnTypeMismatch
behaviour for primitiveint
andlong
types -
under the hood improvements for better performance
-
support for Java 8's
LocalDate
andLocalDateTime
-
introduced
Between
specification which supports allComparable
types. ThereforeDateBetween
is now deprecated -
path variables are now supported! You can use new
pathVars
property of@Spec
as follows:@RequestMapping("/customers/{customerLastName}") @ResponseBody public Object findNotDeletedCustomersByFirstName( @Spec(path = "lastName", pathVars = "customerLastName", spec=Equal.class) Specification<Customer> spec) { return repository.findAll(spec); }
This will handle request
GET /customers/Simpson
asselect c from Customers c where c.lastName = 'Simpson'
. -
better conversion support for
float
,double
andBigDecimal
- bug fixes
-
added
NotEqual
andNotEqualIgnoreCase
specs -
resolving annotations from parent interfaces, for example, consider the following interfaces:
@Spec(path = "deleted", constVal = "false", spec = Equal.class) public interface NotDeletedSpec extends Specification<Customer> {} @Spec(path = "firstName", spec = Equal.class) public interface FirstNameSpec extends NotDeletedSpec {}
FirstNameSpec
extendsNotDeletedSpec
, so their specifications will be combined withand
, i.e. a controller method like this:@RequestMapping("/customers") @ResponseBody public Object findNotDeletedCustomersByFirstName(FirstNameSpec spec) { return repository.findAll(spec); }
will accept HTTP requests such as
GET /customers?firstName=Homer
and execute JPA queries such aswhere firstName = 'Homer' and deleted = false
.
-
join support! It is now possible to filter by attributes of joined entities. For example:
@RequestMapping("/customers") @ResponseBody public Object findByOrders( @Join(path = "orders", alias = "o") @Spec(paths = "o.itemName", params = "orderItem", spec=Like.class) Specification<Customer> spec) { return repository.findAll(spec); }
Of course you can use
@Join
on annotated custom specification interfaces:@Join(path = "orders", alias = "o") @Spec(paths = "o.itemName", params = "orderItem", spec=Like.class) public interface CustomerByOrdersSpec implements Specification<Customer> { } // ... @RequestMapping("/customers") @ResponseBody public Object findByOrders( CustomerByOrdersSpec spec) { return repository.findAll(spec); }
-
@Joins
annotation has been changed to take instances of@Join
asvalue
parameter (was@JoinFetch
).@JoinFetch
might be passed tojoin
param of@Joins
- introduced
NotNull
specification
- bumped dependencies to the latest Spring Boot version and JPA 2.1 API
- added
EqualIgnoreCase
specification - introduced
Null
specification which accepts a boolean HTTP param to dynamically addis null
oris not null
part to the query - introduced
onTypeMismatch
property of@Spec
to define whether an exception should be thrown or empty result returned when an invalid value is passed (e.g. a non numeric value while field type isInteger
). Default behaviour is to return an empty result, which is a breaking change (an exception was thrown in previous versions). UseonTypeMismatch=EXCEPTION
to match old behaviour.
- fixed stack overflow issue with annotated interfaces!
- added
GreaterThan
,GreaterThanOrEqual
,LessThan
,LessThanOrEqual
specs DateAfter
,DateBefore
and their invlusive versions are now deprecated (use the above specs)
- added
@JoinFetch
and@Joins
(see README.md for the details)
- added date inclusive specs
-
it is now allowed to annotate a custom interface that extends
Specification
, eg.:@Or({ @Spec(path="firstName", params="name", spec=Like.class), @Spec(path="lastName", params="name", spec=Like.class) }) public interface FullNameSpec extends Specification<Customer> { }
it can be then used as controller parameter without further annotations, i.e.:
@RequestMapping("/customers") @ResponseBody public Object findByFullName(FullNameSpec spec) { return repository.findAll(spec); }
-
added optional
constVal
attribute in@Spec
. It allows to define a constant part of the query that does not use any HTTP parameters, e.g.:@And({ @Spec(path="deleted", spec=Equal.class, constVal="false"), @Spec(path="firstName", spec=Like.class) })
for handling requests such as
GET /customers?firstName=Homer
and executing queries such as:select c from Customer c where c.firstName like %Homer% and c.deleted = false
-
it is possible to combine parameter and interface annotations. They are combined with 'and' operator. For example you can create a basic interface like:
@Spec(path="deleted", spec=Equal.class, constVal="false") public interface NotDeletedEntitySpec<T> extends Specification<T> {}
And use it for your queries in the controller:
@RequestMapping("/customers") @ResponseBody public Object findNotDeletedCustomerByLastName( @Spec(path="lastName", spec=Equal.class) NotDeletedEntitySpec<Customer> spec) { return repository.findAll(spec); }
-
Equal
andIn
now support boolean values correctly -
introduced
IsNull
specification -
introduced
@Conjunction
and@Disjunction
for nesting ands and ors within each other
- introduced
DateAfter
specification - introduced
Equal
that supports exact match for numbers, strings, dates and enums - introduced
In
that supports in operator for numbers, strings, dates and enums - deprecated
EqualEnum
(useEqual
andIn
)