####Please take a look at the tests for this project for two complete examples usages. One of the examples uses JPA.
There is the ValidationFeatureTest
and the ValidationFeatureJpaTest
that you can run an JUnit tests.
This project was inspired by Stack Overflow question @Valid not throwing exception.
The OP (original poster) is attempting to use @Valid
for bean validation in a Jersey 2.x application.
The problem is that @Valid
is being used in an arbitrary service method.
public class SomeService {
public void save(@Valid Model model) {
...
}
}
This is not supported. What is supported is validation on services that are controlled by Jersey, e.g. resource classes and the methods accepting the incoming DTO. For example
@Path("models")
public class ModelResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response post(@Valid Model model) {
...
}
}
The solution that this project proposes, is to take advantage Jersey's DI implementation HK2. What HK2 offers is an AOP implementation.
What we will do intercept all calls to methods annotated with @Valid
and perform explicit
validation in the interceptor. Here is what it looks like.
public class ValidatingMethodInterceptor implements MethodInterceptor {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object[] args = invocation.getArguments();
for (int i = 0; i < args.length; i++) {
// Java 8
Parameter parameter = invocation.getMethod().getParameters()[i];
if (parameter.getAnnotation(Valid.class) != null) {
handleValidation(args[i]);
}
}
return invocation.proceed();
}
private void handleValidation(Object arg) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(arg);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
}
}
}
The other two things we will need to do is register the interceptor through an implementation
of HK2's InterceptionService
. Here's the implementation
public class ValidationInterceptionService implements InterceptionService {
private final static MethodInterceptor METHOD_INTERCEPTOR = new ValidatingMethodInterceptor();
private final static List<MethodInterceptor> METHOD_LIST = Collections.singletonList(METHOD_INTERCEPTOR);
@Override
public Filter getDescriptorFilter() {
return BuilderHelper.createContractFilter(Validatable.class.getCanonicalName());
}
@Override
public List<MethodInterceptor> getMethodInterceptors(Method method) {
for (Parameter parameter: method.getParameters()) {
if (parameter.isAnnotationPresent(Valid.class)) {
return METHOD_LIST;
}
}
return null;
}
@Override
public List<ConstructorInterceptor> getConstructorInterceptors(Constructor<?> c) {
return null;
}
}
What the getDescriptorFilter()
does is tell HK2 what services should it intercept. In the HK2
documentation, you will see the example return BuilderHelper.allFilter()
. The problem with this
is that HK2 manages a bunch of services in the Jersey space, so having it filter all services
will be a performance hit.
What is introduced in this project is the Validatable
marker interface. In the
getDescriptorFilter()
method, we are saying that any service with an advertised
contract of Validatable
will be intercepted. So two things we need to do is
-
Make our services implement
Validatable
public class SeriviceImpl implements Service, Validatable { ... }
-
Then when we bind the service to HK2, we should bind the implementation to both
Service
andValidatable
bind(ServiceImpl.class).to(Service.class).to(Validatable.class).in(Singleton.class);
Make sure you have Maven, and Java 8 (this project makes use of java.util.reflect.Parameter
)
Get the project
git clone https://github.com/psamsotha/jersey-hk2-validate.git
Build it
cd jersey-hk2-validate
maven clean install
Then include it in your project
<dependency>
<groupId>com.underdog</groupId>
<artifactId>jersey-hk2-validate</artifactId>
<version>0.0.1</version>
</dependency>
To make use of this project, I created a Feature
implementation in ValidationFeature
which takes care of registering all components above. What the user should take care of is:
- Make sure the service they want to bind implements
Validatable
- Make sure the method parameter is annotated with
@Valid
Then with the ValidationFeature
, you can register your services classes. There are three
different APIs you can use to register class. You will first create the Builder
, which is the only
way to create the feature
Builder builder = new ValidationFeature.Builder();
Then you have the following APIs to register services
// registers singleton services by class
builder.addSingletonClass(Implementation.class, ContractClass.class);
// registers singleton services by implementation
builder.addSingletonInstance(instance, ContractClass.class);
// registers request scoped services by class
builder.addRequestScopeClass(Implementation.class, ContractClass.class);
When you are done registering, just call build()
on the builder to get back instance
of ValidationFeature
, which you can register with your application (ResourceConfig
).
ValidationFeature feature = new ValidationFeature.Builder()
.addSingletonClass(SomeService.class, Service.class).build();
ResourceConfig config = new ResourceConfig();
config.register(feature);