Spring MVC Annotated Controller Interface with #PathVariable - spring

Is there any reason not to map Controllers as interfaces?
In all the examples and questions I see surrounding controllers, all are concrete classes. Is there a reason for this? I would like to separate the request mappings from the implementation. I hit a wall though when I tried to get a #PathVariable as a parameter in my concrete class.
My Controller interface looks like this:
#Controller
#RequestMapping("/services/goal/")
public interface GoalService {
#RequestMapping("options/")
#ResponseBody
Map<String, Long> getGoals();
#RequestMapping(value = "{id}/", method = RequestMethod.DELETE)
#ResponseBody
void removeGoal(#PathVariable String id);
}
And the implementing class:
#Component
public class GoalServiceImpl implements GoalService {
/* init code */
public Map<String, Long> getGoals() {
/* method code */
return map;
}
public void removeGoal(String id) {
Goal goal = goalDao.findByPrimaryKey(Long.parseLong(id));
goalDao.remove(goal);
}
}
The getGoals() method works great; the removeGoal(String id) throws an exception
ExceptionHandlerExceptionResolver - Resolving exception from handler [public void
todo.webapp.controllers.services.GoalServiceImpl.removeGoal(java.lang.String)]:
org.springframework.web.bind.MissingServletRequestParameterException: Required
String parameter 'id' is not present
If I add the #PathVariable annotation to the concrete class everything works as expected, but why should i have to re-declare this in the concrete class? Shouldn't it be handled by whatever has the #Controller annotation?

Apparently, when a request pattern is mapped to a method via the #RequestMapping annotation, it is mapped to to the concrete method implementation. So a request that matches the declaration will invoke GoalServiceImpl.removeGoal() directly rather than the method that originally declared the #RequestMapping ie GoalService.removeGoal().
Since an annotation on an interface, interface method, or interface method parameter does not carry over to the implementation there is no way for Spring MVC to recognize this as a #PathVariable unless the implementing class declares it explicitly. Without it, any AOP advice that targets #PathVariable parameters will not be executed.

The feature of defining all bindings on interface actually got implement recently in Spring 5.1.5.
Please see this issue: https://github.com/spring-projects/spring-framework/issues/15682 - it was a struggle :)
Now you can actually do:
#RequestMapping("/random")
public interface RandomDataController {
#RequestMapping(value = "/{type}", method = RequestMethod.GET)
#ResponseBody
RandomData getRandomData(
#PathVariable(value = "type") RandomDataType type, #RequestParam(value = "size", required = false, defaultValue = "10") int size);
}
#Controller
public class RandomDataImpl implements RandomDataController {
#Autowired
private RandomGenerator randomGenerator;
#Override
public RandomData getPathParamRandomData(RandomDataType type, int size) {
return randomGenerator.generateRandomData(type, size);
}
}
You can even use this library: https://github.com/ggeorgovassilis/spring-rest-invoker
To get a client-proxy based on that interface, similarly to how RestEasys client framework works in the JAX-RS land.

It works in newer version of Spring.
import org.springframework.web.bind.annotation.RequestMapping;
public interface TestApi {
#RequestMapping("/test")
public String test();
}
Implement the interface in the Controller
#RestController
#Slf4j
public class TestApiController implements TestApi {
#Override
public String test() {
log.info("In Test");
return "Value";
}
}
It can be used as:
Rest client

Recently I had the same problem. Following has worked for me:
public class GoalServiceImpl implements GoalService {
...
public void removeGoal(#PathVariableString id) {
}
}

i resolved this problem.
ON CLIENT SIDE:
I'm using this library https://github.com/ggeorgovassilis/spring-rest-invoker/. This library generate a proxy from interface to invoke spring rest service.
I extended this library:
I created an annotations and a factory client class:
Identify a Spring Rest Service
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface SpringRestService {
String baseUri();
}
This class generates a client rest from interfaces
public class RestFactory implements BeanFactoryPostProcessor,EmbeddedValueResolverAware {
StringValueResolver resolver;
#Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.resolver = resolver;
}
private String basePackage = "com";
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
createBeanProxy(beanFactory,SpringRestService.class);
createBeanProxy(beanFactory,JaxrsRestService.class);
}
private void createBeanProxy(ConfigurableListableBeanFactory beanFactory,Class<? extends Annotation> annotation) {
List<Class<Object>> classes;
try {
classes = AnnotationUtils.findAnnotatedClasses(basePackage, annotation);
} catch (Exception e) {
throw new BeanInstantiationException(annotation, e.getMessage(), e);
}
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (Class<Object> classType : classes) {
Annotation typeService = classType.getAnnotation(annotation);
GenericBeanDefinition beanDef = new GenericBeanDefinition();
beanDef.setBeanClass(getQueryServiceFactory(classType, typeService));
ConstructorArgumentValues cav = new ConstructorArgumentValues();
cav.addIndexedArgumentValue(0, classType);
cav.addIndexedArgumentValue(1, baseUri(classType,typeService));
beanDef.setConstructorArgumentValues(cav);
registry.registerBeanDefinition(classType.getName() + "Proxy", beanDef);
}
}
private String baseUri(Class<Object> c,Annotation typeService){
String baseUri = null;
if(typeService instanceof SpringRestService){
baseUri = ((SpringRestService)typeService).baseUri();
}else if(typeService instanceof JaxrsRestService){
baseUri = ((JaxrsRestService)typeService).baseUri();
}
if(baseUri!=null && !baseUri.isEmpty()){
return baseUri = resolver.resolveStringValue(baseUri);
}else{
throw new IllegalStateException("Impossibile individuare una baseUri per l'interface :"+c);
}
}
private static Class<? extends FactoryBean<?>> getQueryServiceFactory(Class<Object> c,Annotation typeService){
if(typeService instanceof SpringRestService){
return it.eng.rete2i.springjsonmapper.spring.SpringRestInvokerProxyFactoryBean.class;
}else if(typeService instanceof JaxrsRestService){
return it.eng.rete2i.springjsonmapper.jaxrs.JaxRsInvokerProxyFactoryBean.class;
}
throw new IllegalStateException("Impossibile individuare una classe per l'interface :"+c);
}
}
I configure my factory:
<bean class="it.eng.rete2i.springjsonmapper.factory.RestFactory">
<property name="basePackage" value="it.giancarlo.rest.services" />
</bean>
ON REST SERVICE SIGNATURE
this is an example interface:
package it.giancarlo.rest.services.spring;
import ...
#SpringRestService(baseUri="${bookservice.url}")
public interface BookService{
#Override
#RequestMapping("/volumes")
QueryResult findBooksByTitle(#RequestParam("q") String q);
#Override
#RequestMapping("/volumes/{id}")
Item findBookById(#PathVariable("id") String id);
}
ON REST SERVICE IMPLEMENTATION
Service implementation
#RestController
#RequestMapping("bookService")
public class BookServiceImpl implements BookService {
#Override
public QueryResult findBooksByTitle(String q) {
// TODO Auto-generated method stub
return null;
}
#Override
public Item findBookById(String id) {
// TODO Auto-generated method stub
return null;
}
}
To resolve annotation on parameters I create a custom RequestMappingHandlerMapping that looks all interfaces annotated with #SpringRestService
public class RestServiceRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
public HandlerMethod testCreateHandlerMethod(Object handler, Method method){
return createHandlerMethod(handler, method);
}
#Override
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
handlerMethod = new RestServiceHandlerMethod(beanName,getApplicationContext().getAutowireCapableBeanFactory(), method);
}
else {
handlerMethod = new RestServiceHandlerMethod(handler, method);
}
return handlerMethod;
}
public static class RestServiceHandlerMethod extends HandlerMethod{
private Method interfaceMethod;
public RestServiceHandlerMethod(Object bean, Method method) {
super(bean,method);
changeType();
}
public RestServiceHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean,methodName,parameterTypes);
changeType();
}
public RestServiceHandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
super(beanName,beanFactory,method);
changeType();
}
private void changeType(){
for(Class<?> clazz : getMethod().getDeclaringClass().getInterfaces()){
if(clazz.isAnnotationPresent(SpringRestService.class)){
try{
interfaceMethod = clazz.getMethod(getMethod().getName(), getMethod().getParameterTypes());
break;
}catch(NoSuchMethodException e){
}
}
}
MethodParameter[] params = super.getMethodParameters();
for(int i=0;i<params.length;i++){
params[i] = new RestServiceMethodParameter(params[i]);
}
}
private class RestServiceMethodParameter extends MethodParameter{
private volatile Annotation[] parameterAnnotations;
public RestServiceMethodParameter(MethodParameter methodParameter){
super(methodParameter);
}
#Override
public Annotation[] getParameterAnnotations() {
if (this.parameterAnnotations == null){
if(RestServiceHandlerMethod.this.interfaceMethod!=null) {
Annotation[][] annotationArray = RestServiceHandlerMethod.this.interfaceMethod.getParameterAnnotations();
if (this.getParameterIndex() >= 0 && this.getParameterIndex() < annotationArray.length) {
this.parameterAnnotations = annotationArray[this.getParameterIndex()];
}
else {
this.parameterAnnotations = new Annotation[0];
}
}else{
this.parameterAnnotations = super.getParameterAnnotations();
}
}
return this.parameterAnnotations;
}
}
}
}
I created a configuration class
#Configuration
public class WebConfig extends WebMvcConfigurationSupport{
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RestServiceRequestMappingHandlerMapping handlerMapping = new RestServiceRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager());
PathMatchConfigurer configurer = getPathMatchConfigurer();
if (configurer.isUseSuffixPatternMatch() != null) {
handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
}
if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
handlerMapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
}
if (configurer.isUseTrailingSlashMatch() != null) {
handlerMapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
}
if (configurer.getPathMatcher() != null) {
handlerMapping.setPathMatcher(configurer.getPathMatcher());
}
if (configurer.getUrlPathHelper() != null) {
handlerMapping.setUrlPathHelper(configurer.getUrlPathHelper());
}
return handlerMapping;
}
}
and I configurated it
<bean class="....WebConfig" />

Related

Injecting spring bean (service layer class) into ResourceBundle

I created a class using ResourceBundle interface as shown below. This class is dependent on another class. The implementation class for ResourceBundle (QuestionnaireSource as shown below) always has null as dependencies. No matter if I use setter or constructor injection.
Could someone please help me with this issue. I am I missing some configuration here.
#Component
public class QuestionnaireSource extends ResourceBundle {
private final QuestionnaireCache questionnaireCache;
private static final Object lockObject = new Object();
#Override
protected Object handleGetObject(String key) {
// Gets an object for the given key from this resource bundle.
// Returns null if this resource bundle does not contain an object for the given key 0
Object value = null;
try {
value = getString(key, LocaleContextHolder.getLocale());
} catch (Exception ignored) {
}
return value;
}
public Questionnaire getString(String key, Locale locale) {
Locale l = safeLocale(locale);
return getResources(l).get(key);
}
private Locale safeLocale(Locale l) {
if (l.getLanguage().equalsIgnoreCase("DE")) {
return Locale.GERMAN;
} else {
return Locale.ENGLISH;
}
}
protected Map<String, Questionnaire> getResources(Locale locale) {
synchronized (lockObject) {
return questionnaireCache.getQuestionnaireCache().get(locale.getLanguage().toUpperCase());
}
}
#Override
public Enumeration<String> getKeys() {
return null;
}
public QuestionnaireSource(QuestionnaireCache questionnaireCache) {
super();
this.questionnaireCache = questionnaireCache;
}
}
Update:
I found that even simple dependency injection in resourceBundle is failing.
UPdate2:
The way I am using in the main class is as follows:
// ResourceBundle test here
System.out.println("Test here for resource bundle");
Locale locale = new Locale("de", "DE");
ResourceBundle bundle = ResourceBundle.getBundle("com.app.util.QuestionnaireSource", locale);
System.out.println(bundle.getString("some.test.string"));
Update3
I am writing a simple example to convey the scenario:
Some service class
#Service
public class SomeServiceTest {
public String testMethod(){
return "test here and complete";
}
}
Some example implementation of resource bundle
#Component
public class MyResourceBundle extends ResourceBundle {
private final SomeServiceTest someServiceTest;
#Autowired
public MyResourceBundle(SomeServiceTest someServiceTest) {
this.someServiceTest = someServiceTest;
}
#Override
protected Object handleGetObject(String key) {
if(key.equals("test"))
return "test";
return null;
}
#Override
public Enumeration<String> getKeys() {
return null;
}
}
Main.java
main(){
// ResourceBundle test here
System.out.println("Test here for resource bundle");
Locale locale = new Locale("de", "DE");
ResourceBundle bundle = ResourceBundle.getBundle("com.app.util.MyResourceBundle", locale);
System.out.println(bundle.getString("test"));
}
Update4:
I changed the annotation on classes as mentioned by on this post https://www.baeldung.com/spring-inject-bean-into-unmanaged-objects
but still I have the null dependency injection for SomeServiceTest class. The changes are as shown below.
SomeServiceTest.java
#Service
public class SomeServiceTest {
public String testMethod(){
return "test here and complete";
}
}
MyResourceBundle.java
#Configurable
public class MyResourceBundle extends ResourceBundle {
#Autowired
private SomeServiceTest someServiceTest;
public MyResourceBundle() {
}
#Override
protected Object handleGetObject(String key) {
if(key.equals("test"))
return someServiceTest.testMethod();
return null;
}
#Override
public Enumeration<String> getKeys() {
return null;
}
}
still SomeServiceTest class is null.
Can you please post an example on how you are using this class? Is it you (your code) or spring who instanciate it (on startup)?
#Component only works for beans which Spring instanciate. If you want to inject stuff in classes you instanciate in you code you can annotate the class with #Configurable.
Please see https://www.baeldung.com/spring-inject-bean-into-unmanaged-objects for some examples.
Make sure you have initialized the spring context
If you are using spring boot
You can get the application context after it starts and use it to get the bean you want
For example
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(YouApplication.class, args);
MyResourceBundle resConfig = run.getBean("myResourceBundle", MyResourceBundle .class);
resConfig.handleGetObject("test");
}
Unfortunately ResourceBundle.getBundle does not initialize the spring application context

Spring Security - Process custom annotation in GenericFilterBean

in my controller I have a custom annotation like:
#GetMapping("/apikey")
#Secured(apiKeys = { ApiKey.APP_1}) // <- Custom annotation
public ResponseEntity startApiKey() {
return ResponseEntity.status(HttpStatus.OK).body("ApiKey approved");
}
In my Spring Security Config I have added a Filter for checking the apikey and authentication:
public class ApiKeyAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(ApiKeyHeadername.DEFAULTHEADERNAME.getHeadername());
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authentication -> {
String principal = (String) authentication.getPrincipal();
if (!ApiKey.APP_1.getApiKey().equals(principal))
{
throw new BadCredentialsException("The API key was not found or not the expected value.");
}
authentication.setAuthenticated(true);
return authentication;
});
}
}
Before the custom annotation was proccessed within a AspectJ class:
#Component
#Aspect
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SecurityAspect {
#Autowired
private IPrincipal principal;
#Autowired
private AuthorizationManager authorizationManager;
#Pointcut("#annotation(my.demo.application.security.aspect.Secured)")
public void methodAnnotatedWithSecured() {
}
#Around("methodAnnotatedWithSecured()")
public Object userAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Secured securedAnnotation = method.getAnnotation(Secured.class);
Authorized securityInformation = new Authorized(securedAnnotation.apiKeys(), securedAnnotation.roles(),
securedAnnotation.usernames());
if (authorizationManager.authorizeUserPrincipal(principal,
securityInformation) == AuthorizationState.UNAUTHORIZED) {
throw DefaultNotAuthorizedExceptionFactory.createNotAuthorizedException();
}
return joinPoint.proceed();
}
}
How can I process the annotation informations in the AbstractPreAuthenticatedProcessingFilter, or how can i get the annotation by Reflection in this Filter. Or can I inject something to get it?
Thank you in advice

Bean not getting overridden in Spring boot

I am trying to write and test an application that used spring-cloud with azure functions following this tutorial.
https://github.com/markusgulden/aws-tutorials/tree/master/spring-cloud-function/spring-cloud-function-azure/src/main/java/de/margul/awstutorials/springcloudfunction/azure
I am tryign to write a testcase and override the bean.
Here is the application class having function and handler Bean function.
#SpringBootApplication
#ComponentScan(basePackages = { "com.package" })
public class DataFunctions extends AzureSpringBootRequestHandler<GenericMessage<Optional<String>>, Data> {
#FunctionName("addData")
public HttpResponseMessage addDataRun(
#HttpTrigger(name = "add", methods = {
HttpMethod.POST }, authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) throws JsonParseException, JsonMappingException, IOException {
context.getLogger().info("Java HTTP trigger processed a POST request.");
try {
handleRequest(new GenericMessage<Optional<String>>(request.getBody()), context);
} catch (ServiceException ex) {
ErrorMessage em = new ErrorMessage();
return request.createResponseBuilder(handleException(ex, em)).body(em).build();
}
return request.createResponseBuilder(HttpStatus.CREATED).build();
}
#Autowired
MyService mService;
#Bean
public Consumer<GenericMessage<Optional<String>>> addData() {
ObjectMapper mapper = new ObjectMapper();
return req -> {
SomeModel fp = null;
try {
fp = mapper.readValue(req.getPayload().get(), SomeModel.class);
} catch (Exception e) {
throw new ServiceException(e);
}
mService.addData(fp);
};
}
}
I want to test by overriding the above bean.
Cosmosdb spring configuration
#Configuration
#EnableDocumentDbRepositories
public class CosmosDBConfig extends AbstractDocumentDbConfiguration {
#Value("${cosmosdb.collection.endpoint}")
private String uri;
#Value("${cosmosdb.collection.key}")
private String key;
#Value("${cosmosdb.collection.dbname}")
private String dbName;
#Value("${cosmosdb.connect.directly}")
private Boolean connectDirectly;
#Override
public DocumentDBConfig getConfig() {
ConnectionPolicy cp = ConnectionPolicy.GetDefault();
if (connectDirectly) {
cp.setConnectionMode(ConnectionMode.DirectHttps);
} else {
cp.setConnectionMode(ConnectionMode.Gateway);
}
return DocumentDBConfig.builder(uri, key, dbName).connectionPolicy(cp).build();
}
}
Here is the configuration
#TestConfiguration
#PropertySource(value = "classpath:application.properties", encoding = "UTF-8")
#Profile("test")
#Import({DataFunctions.class})
public class TestConfig {
#Bean(name="addData")
#Primary
public Consumer<GenericMessage<Optional<String>>> addData() {
return req -> {
System.out.println("data mock");
};
}
#Bean
#Primary
public DocumentDBConfig getConfig() {
return Mockito.mock(DocumentDBConfig.class);
}
}
Finally the test class
#RunWith(SpringRunner.class)
//#SpringBootTest //Enabling this gives initialization error.
#ActiveProfiles("test")
public class TempTest {
#InjectMocks
DataFunctions func;
#Mock
MyService mService;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
private Optional<String> createRequestString(final String res) throws IOException {
InputStream iStream = TempTest.class.getResourceAsStream(res);
String charset="UTF-8";
try (BufferedReader br = new BufferedReader(new InputStreamReader(iStream, charset))) {
return Optional.of(br.lines().collect(Collectors.joining(System.lineSeparator())));
}
}
#Test
public void testHttpPostTriggerJava() throws Exception {
#SuppressWarnings("unchecked")
final HttpRequestMessage<Optional<String>> req = mock(HttpRequestMessage.class);
final Optional<String> queryBody = createRequestString("/test-data.json");
doNothing().when(mService).addData(Mockito.any(SomeModel.class));
doReturn(queryBody).when(req).getBody();
doAnswer(new Answer<HttpResponseMessage.Builder>() {
#Override
public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
HttpStatus status = (HttpStatus) invocation.getArguments()[0];
return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
}
}).when(req).createResponseBuilder(any(HttpStatus.class));
final ExecutionContext context = mock(ExecutionContext.class);
doReturn(Logger.getGlobal()).when(context).getLogger();
doReturn("addData").when(context).getFunctionName();
// Invoke
final HttpResponseMessage ret = func.addDataRun(req, context);
// Verify
assertEquals(ret.getStatus(), HttpStatus.CREATED);
}
}
For this case instead of test configuration addData the actual bean is called from DataFunctions class. Also the database connection is also created when it should use the mocked bean from my test configuration. Can somebody please point out what is wrong in my test configuration?
I was able to resolve the first part of cosmos db config loading by marking it with
#Configuration
#EnableDocumentDbRepositories
#Profile("!test")
public class CosmosDBConfig extends AbstractDocumentDbConfiguration {
...
}
Also had to mark the repository bean as optional in the service.
public class MyService {
#Autowired(required = false)
private MyRepository myRepo;
}
Didn't use any spring boot configuration other than this.
#ActiveProfiles("test")
public class FunctionTest {
...
}
For the second part of providing mock version of Mock handlers, I simply made the test config file as spring application as below.
#SpringBootApplication
#ComponentScan(basePackages = { "com.boeing.da.helix.utm.traffic" })
#Profile("test")
public class TestConfiguration {
public static void main(final String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
#Bean(name="addData")
#Primary
public Consumer<GenericMessage<Optional<String>>> addData() {
return req -> {
System.out.println("data mock");
};
}
}
and made use of this constructor from azure functions library in spring cloud in my constructor
public class AppFunctions
extends AzureSpringBootRequestHandler<GenericMessage<Optional<String>>, List<Data>> {
public AppFunctions(Class<?> configurationClass) {
super(configurationClass);
}
}
public AzureSpringBootRequestHandler(Class<?> configurationClass) {
super(configurationClass);
}
Hope it helps someone.

spring-data-rest: Validator not being invoked

I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:
ValidatorRegistrar: Workaround for a bug
#Configuration
public class ValidatorRegistrar implements InitializingBean {
private static final List<String> EVENTS;
static {
List<String> events = new ArrayList<String>();
events.add("beforeCreate");
events.add("afterCreate");
events.add("beforeSave");
events.add("afterSave");
events.add("beforeLinkSave");
events.add("afterLinkSave");
events.add("beforeDelete");
events.add("afterDelete");
EVENTS = Collections.unmodifiableList(events);
}
#Autowired
ListableBeanFactory beanFactory;
#Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
#Override
public void afterPropertiesSet() throws Exception {
Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
.ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
}
}
}
Validator class:
#Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Bid.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Bid bid = (Bid)target;
if (!bid.getAddendaAcknowledged()) {
errors.rejectValue("addendaAcknowledged",
"addendaAcknowledged is not true");
}
}
}
Custom RestController for Bids:
#RestController
#RequestMapping(path = "/bids")
public class BidController {
private BidRepository bidRepository;
#Autowired
public BidController(
BidRepository bidRepository) {
this.bidRepository = bidRepository;
}
#PutMapping("{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
Rest Client Test Code:
Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)
Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
"/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)
// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.
Any idea what I am missing?
You are trying to use your validator with custom controller, not SDR controller. In this case you can just add it to your controller with #InitBinder annotation:
#RestController
#RequestMapping("/bids")
public class BidController {
//...
#InitBinder("bid") // add this parameter to apply this binder only to request parameters with this name
protected void bidValidator(WebDataBinder binder) {
binder.addValidators(new BidValidator());
}
#PutMapping("/{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
#Component annotation on your validator is not necessary as well as ValidatorRegistrar class.
How to use validators with SDR controllers you can read in my another answer.

hibernate-envers RevisionListener spring integration as spring bean

I need to do some database processing in revision listener of hibernate-envers. For that I need inejction capabilities of Spring Framework. How can this be implemented? Here is the code representing the need but CustomRevisionListener is instantiated by a constructor in Envers code. Spring has only SecurityContextHolder as static service locator. How to inject other beans?
#Service
public class CustomRevisionListener implements EntityTrackingRevisionListener {
#Resource
private PersistenceManagerHibernate persistenceManagerHibernate;
public void newRevision(Object revisionEntity) {
CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity;
revision.setUsername(getUsername());
}
public String getUsername() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
if (context.getAuthentication() != null) {
return context.getAuthentication().getName();
} else {
return "anonymous";
}
}
return "anonymous";
}
#Override
public void entityChanged(#SuppressWarnings("rawtypes") Class entityClass, String entityName, Serializable entityId, RevisionType revisionType, Object revisionEntity) {
CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity;
revision.setEntityId(entityId.toString());
revision.setEntityName(entityName);
revision.setRevisionType((int)revisionType.getRepresentation());
Auditable auditable = null;
if (entityId instanceof Long) {
auditable = persistenceManagerHibernate.findById(entityClass, (Long)entityId);
}
revision.setGroupName(auditable.getAuditGroupName());
revision.setGroupEntityId(auditable.getAuditGroupId());
}
}
Since CustomRevisionListener is instantiated by a constructor in Envers, you have to find another way to retrieve a handle to a Spring managed bean.
You could create a utility class to achieve this:
/**
* Utility class which provides lookup services for Spring managed beans from
* within unmanaged beans.
*/
#Component
public class ContextLookup implements ApplicationContextAware {
private static ApplicationContext sApplicationContext;
#Override
public void setApplicationContext( ApplicationContext aApplicationContext )
throws BeansException {
setContext( aApplicationContext );
}
public static void setContext( ApplicationContext aApplicationContext ) {
sApplicationContext = aApplicationContext;
}
protected static ApplicationContext getApplicationContext() {
return sApplicationContext;
}
public static Object getBean( String aName ) {
if ( sApplicationContext != null ) {
return sApplicationContext.getBean( aName );
}
return null;
}
public static <T> T getBean( Class<T> aClass ) {
if ( sApplicationContext != null ) {
return sApplicationContext.getBean( aClass );
}
return null;
}
}
and in your CustomRevisionListener
public class CustomRevisionListener {
public void myMethod() {
..
// get a handle to your spring managed bean
Foo foo = (Foo)ContextLookup.getBean( "mySpringManagedBean" );
..
}
}

Resources