Spring Around Aspect does not get called in controller - java

The problem is that when I want to print some logs from some controller methods the around annotated method with LogInit annotation does not get called, but when I am annotating some other service method with the same annotation the around method does get called. I am stuck for some time in this problem and I need to make it work.
I have the following aspect:
package com.db.mybank.backend.aop;
import com.db.mybank.backend.model.presentation.CustomerDataVO;
//import org.apache.log4j.LogManager;
import org.apache.log4j.MDC;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
#Aspect
#Component
public class LogInitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger("SPLUNK");
private static final String METRIC_SPLITTER = ";";
/**
* Creates AOP pointcut and advice
* #param joinPoint (the executing method which has been annotated with #LogInit )
* #return
* #throws Throwable
*/
#Around("#annotation(com.db.mybank.backend.aop.LogInit)")
public void logInitSplunk(ProceedingJoinPoint joinPoint) {
initMDC();
CustomerDataVO proceed = null;
try {
proceed = (CustomerDataVO) joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LOGGER.info(getFormattedLogLine());
return;
}
private String getFormattedLogLine() {
return MDC.get(MDCEnum.TIMESTAMP.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.CCCARD.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.SESSIONID.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.TIMESTAMP_REQUEST.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.TIMESTAMP_RESPONSE.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.SERVICE_ID.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.SERVICE_URI.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.RESULT.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.DEVICE_INFO.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.LOGIN_TIMESTAMP.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.RESULT_LOGIN.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.LOGOUT_TIMESTAMP.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.IP_ADDRESS_CLIENT.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.USER_AGENT_CLIENT.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.TIMESTAMP_AUTHORIZATION.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.TRANSACTION_SERVICE_NAME.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.VALUE_AND_CURRENCY.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.TRANSACTION_ID.getValue())+METRIC_SPLITTER
+MDC.get(MDCEnum.TIMESTAMP_TRANSACTION_SUBMIT_TO_BACKEND.getValue())+METRIC_SPLITTER+MDC.get(MDCEnum.NDG.getValue())+METRIC_SPLITTER;
}
private void initMDC(){
MDC.put(MDCEnum.TIMESTAMP.getValue(),"N/A");
MDC.put(MDCEnum.CCCARD.getValue(),"N/A");
MDC.put(MDCEnum.SESSIONID.getValue(),"N/A");
MDC.put(MDCEnum.TIMESTAMP_REQUEST.getValue(),"N/A");
MDC.put(MDCEnum.TIMESTAMP_RESPONSE.getValue(),"N/A");
MDC.put(MDCEnum.SERVICE_ID.getValue(),"N/A");
MDC.put(MDCEnum.SERVICE_URI.getValue(),"N/A");
MDC.put(MDCEnum.RESULT.getValue(),"N/A");
MDC.put(MDCEnum.DEVICE_INFO.getValue(),"N/A");
MDC.put(MDCEnum.LOGIN_TIMESTAMP.getValue(),"N/A");
MDC.put(MDCEnum.RESULT_LOGIN.getValue(),"N/A");
MDC.put(MDCEnum.LOGOUT_TIMESTAMP.getValue(),"N/A");
MDC.put(MDCEnum.IP_ADDRESS_CLIENT.getValue(),"N/A");
MDC.put(MDCEnum.USER_AGENT_CLIENT.getValue(),"N/A");
MDC.put(MDCEnum.TIMESTAMP_AUTHORIZATION.getValue(),"N/A");
MDC.put(MDCEnum.TRANSACTION_SERVICE_NAME.getValue(),"N/A");
MDC.put(MDCEnum.VALUE_AND_CURRENCY.getValue(),"N/A");
MDC.put(MDCEnum.TRANSACTION_ID.getValue(),"N/A");
MDC.put(MDCEnum.TIMESTAMP_TRANSACTION_SUBMIT_TO_BACKEND.getValue(),"N/A");
MDC.put(MDCEnum.NDG.getValue(),"N/A");
}
}
And I have the following controller method which is not working.
#RequestMapping(value = "/prelogin", method = RequestMethod.POST)
#ResponseBody
#LogInit
public AuthDataVO preLogin(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {

Related

How to test Aspects

I have found and tried to follow this answer by Roman Puchkovskiy with a detailed example, but I am missing some detail.
Here is the aspect I am trying to test:
package com.company.reporting.logger.consumer.prometheusmetrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.kafka.KafkaException;
import org.springframework.stereotype.Component;
#Aspect
#Component
#Slf4j
/**
* AOP Class to capture error count due to kafka exception
*/
public class MetricsCollection {
MeterRegistry meterRegistry;
private final Counter counter;
public MetricsCollection(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
counter = Counter.builder("Kafka.producer.error.count").description("Custom Kafka Producer metrics for business exception").tags("behavior", "exception").register(meterRegistry);
}
/***
* point cut for jointPoint within service class execution
*/
#Pointcut("within (#org.springframework.stereotype.Service *)")
public void serviceBean() {
// this is pointcut
}
/***
* point cut for all the jointPoints
*/
#Pointcut("execution(* *(..))")
public void methodPointcut() {
// this is pointcut
}
/***
*
* increasing counter upon kafka exception
*/
#AfterThrowing(pointcut = "serviceBean() && methodPointcut()", throwing = "e")
public void handleException(Exception e) {
if (e instanceof KafkaException || e instanceof org.apache.kafka.common.KafkaException) {
LOGGER.error("*** In Aspect ErrorHandler *** " + e.getLocalizedMessage());
counter.increment();
}
}
}
And here is my unit test class:
package com.company.reporting.logger.consumer.prometheusmetrics;
import com.company.reporting.logger.consumer.utils.TestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import org.apache.kafka.common.KafkaException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.framework.DefaultAopProxyFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
class MetricsCollectionTest {
private MetricsCollection aspect;
private TestController controllerProxy;
MeterRegistry meterRegistry;
#BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
meterRegistry = new CompositeMeterRegistry();
aspect = new MetricsCollection(meterRegistry);
AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new TestController());
aspectJProxyFactory.addAspect(aspect);
DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
AopProxy aopProxy = proxyFactory.createAopProxy(aspectJProxyFactory);
controllerProxy = (TestController) aopProxy.getProxy();
}
#Test
void MetricsCollection() {
MetricsCollection metricsCollection = new MetricsCollection(meterRegistry);
assertNotNull(metricsCollection);
}
#Test
void handleException() {
ListAppender<ILoggingEvent> listAppender = TestUtils.getiLoggingEventListAppender(MetricsCollection.class);
try {
controllerProxy.throwKafkaException();
} catch (Exception ex) {
if (! (ex instanceof KafkaException)) {
fail();
}
} finally {
List<ILoggingEvent> logList = listAppender.list;
assertEquals(1, logList.size());
}
}
#Controller
static
class TestController {
#Bean
String throwKafkaException() {
throw new KafkaException();
}
}
}
Finally, here is my TestUtils class:
package com.company.reporting.logger.consumer.utils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;
public class TestUtils {
#NotNull
public static ListAppender<ILoggingEvent> getiLoggingEventListAppender(Class clazz) {
Logger logger = (Logger) LoggerFactory.getLogger(clazz);
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.setName(clazz.getName());
listAppender.start();
logger.addAppender(listAppender);
return listAppender;
}
}
My constructor test passes... :)
But the handleException() test is failing to trigger my aspect. Which dot on an i or cross bar on a t did I miss?
Your "unit test" is closer to an integration test, and you are testing the AOP framework more than the aspect itself. The only thing you do seem to test is the side effect of logging, which is a topic unrelated to AOP. For other ways to unit-test or integration-test aspects, see my linked answers.
Apart from that and without having tried to copy and compile your code yet, what immediately strikes me as odd is that your aspect has a within (#org.springframework.stereotype.Service *) pointcut, but your target class seems to be a #Controller. I would not expect the aspect to match there. What happens if you fix that?

Why AfterReturning PointCut is not working?

My pointcut for method public int quantity() in ShoppingCart.java is not working. I am trying to create a pointcut to capture the return value of the method public int quantity() but the relevant advice for #AfterReturning pointcut doesn't seem to get invoked even after the quantity method is invoked by main method (See the file AuthenticationAspect.java which is trying to define an afterReturning Pointcut).
File ShoppingCart.java
package org.example;
import org.springframework.stereotype.Component;
#Component
public class ShoppingCart {
public int quantity()
{
System.out.println("\n Quantity method called");
return 2;
}
}
AuthenticationAspect.java
package org.example;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
#Aspect
#Component
public class AuthenticationAspect {
#Pointcut("execution(* org.example.ShoppingCart.quantity())")
public void afterReturningPointCut()
{
}
#AfterReturning(pointcut = "afterReturningPointCut()", returning = "retVal")
public void afterReturning(String retVal)
{
System.out.println("The quantity used is " + retVal);
}
}
Main.java
package org.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
ShoppingCart cart = context.getBean(ShoppingCart.class);
cart.quantity();
}
}
A returning clause restricts matching to only those method executions that return a value of the specified type.
The ShoppingCart.quantity() method returns int, therefore it doesn't match the String return type declared in the advice. You can change the retVal type to int or to Object, which matches any return value.
For reference: After Returning Advice

#ControllerAdvice annotated class is not catching the exception thrown in the service layer

I'm trying to centralize the error handling in my spring boot app. Currently i'm only handling one potential exception (NoSuchElementException), this is the controller advice:
import java.util.NoSuchElementException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
#ControllerAdvice
public class ExceptionController {
#ExceptionHandler(NoSuchElementException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public DispatchError dispatchNotFound(NoSuchElementException exception) {
System.out.println("asdasdasd");
return new DispatchError(exception.getMessage());
}
}
And here's the service which throws the exceptions:
import java.util.List;
import com.deliveryman.deliverymanapi.model.entities.Dispatch;
import com.deliveryman.deliverymanapi.model.repositories.DispatchRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#Service
public class DaoService {
#Autowired
DispatchRepository dispatchRepo;
public Dispatch findByShipmentNumber(long shipmentNumber) {
return dispatchRepo.findById(shipmentNumber).orElseThrow();
}
public List<Dispatch> findByUser(String user, String status) {
if(status == null) {
return dispatchRepo.findByOriginator(user).orElseThrow();
} else {
return dispatchRepo.findByOriginatorAndStatus(user, status).orElseThrow();
}
}
public Dispatch createDispatch(Dispatch dispatch) { //TODO parameter null check exception
return dispatchRepo.save(dispatch);
}
}
The problem is that once I send a request for an inexistent resource, the json message shown is the spring's default one. It should be my custom json error message (DispatchError).
Now, this is fixed by adding a #ResponseBody to the exception handler method but the thing is that I was using an old code of mine as reference, which works as expected without the #ResponseBody annotation.
Can someone explain me why this is happening?
Either annotate your controller advice class with #ResponseBody
#ControllerAdvice
#ResponseBody
public class ExceptionController {
...
or replace #ControllerAdvice with #RestControllerAdvice.
Tested and verified on my computer with source from your controller advice.
From source for #RestControllerAdvice
#ControllerAdvice
#ResponseBody
public #interface RestControllerAdvice {
...
Hence, #RestControllerAdvice is shorthand for
#ControllerAdvice
#ResponseBody
From source doc for #ResponseBody
Annotation that indicates a method return value should be bound to the
web response body. Supported for annotated handler methods.
Alternative using #ControllerAdvice only:
#ControllerAdvice
public class ExceptionHandlerAdvice {
#ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<DispatchError> dispatchNotFound(NoSuchElementException exception) {
return new ResponseEntity<>(new DispatchError(exception.getMessage()), HttpStatus.NOT_FOUND);
}
}
I do have a theory on what's going on in your old app. With the advice from your question, and the error handler below, I can create a behaviour where the DispatchError instance appears to be returned by advice (advice is executed), but is actually returned by error controller.
package no.mycompany.myapp.error;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
#RestController
#RequiredArgsConstructor
public class ErrorHandler implements ErrorController {
private static final String ERROR_PATH = "/error";
private final ErrorAttributes errorAttributes;
#RequestMapping(ERROR_PATH)
DispatchError handleError(WebRequest webRequest) {
var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
return new DispatchError((String) attrs.get("message"));
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
Putting an implementation of ErrorController into classpath, replaces Spring's BasicErrorController.
When reinforcing #RestControllerAdvice, error controller is no longer in effect for NoSuchElementException.
In most cases, an ErrorController implementation that handles all errors, in combination with advice exception handlers for more complex exceptions like MethodArgumentNotValidException, should be sufficient. This will require a generic error DTO like this
package no.mycompany.myapp.error;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.Map;
#Data
#NoArgsConstructor
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiError {
private long timestamp = new Date().getTime();
private int status;
private String message;
private String url;
private Map<String, String> validationErrors;
public ApiError(int status, String message, String url) {
this.status = status;
this.message = message;
this.url = url;
}
public ApiError(int status, String message, String url, Map<String, String> validationErrors) {
this(status, message, url);
this.validationErrors = validationErrors;
}
}
For ErrorHandler above, replace handleError with this
#RequestMapping(ERROR_PATH)
ApiError handleError(WebRequest webRequest) {
var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
return new ApiError(
(Integer) attrs.get("status"),
(String) attrs.get("message"), // consider using predefined message(s) here
(String) attrs.get("path"));
}
Advice with validation exception handling
package no.mycompany.myapp.error;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
#RestControllerAdvice
public class ExceptionHandlerAdvice {
private static final String ERROR_MSG = "validation error";
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
ApiError handleValidationException(MethodArgumentNotValidException exception, HttpServletRequest request) {
return new ApiError(
HttpStatus.BAD_REQUEST.value(),
ERROR_MSG,
request.getServletPath(),
exception.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
// mergeFunction handling multiple errors for a field
(firstMessage, secondMessage) -> firstMessage)));
}
}
Related config in application.yml
server:
error:
include-message: always
include-binding-errors: always
When using application.properties
server.error.include-message=always
server.error.include-binding-errors=always
When using Spring Data JPA, consider using the following setting for turning off a second validation.
spring:
jpa:
properties:
javax:
persistence:
validation:
mode: none
More information on exception handling in Spring:
https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc (revised April 2018)
https://www.baeldung.com/exception-handling-for-rest-with-spring (December 31, 2020)

Spring AOP. How to make pointcut for all public methods in annotated class

I need to handle all the exceptions that are thrown from public methods of class annotated with some annotation.
I trying to use Spring AOP. This is my logger:
#Aspect
public class Logger {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#Pointcut("#annotation(loggable)")
public void isLoggable(Loggable loggable) {
}
#AfterThrowing(pointcut = "isLoggable(loggable)", throwing = "e")
public void afterThrowing(Loggable loggable, Exception e) throws Throwable {
log.error("AFTER", e);
}
#Loggable is my annotation.
Then I've added #EnableAspectJAutoProxy annotation to my configuration class.
First I've tried to annotate some method that throws an exception. It works fine but how can I make this work for all public methods in class annotated with #Loggable annotation?
You can create the aspect like this, where #LogMe is the annotation:
#Pointcut("execution(#LogMe * *(..))") to match all the public methods.
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
#Aspect
#Component
public class LogExecutionTime {
private static final String LOG_MESSAGE_FORMAT = "%s.%s execution time: %dms";
private static final Logger logger = LoggerFactory.getLogger(LogExecutionTime.class);
#Pointcut("execution(#LogMe * *(..))")
public void isAnnotated() {}
/**
* Method will add log statement of running time of the methods which are annotated with #LogMe
* #param joinPoint
* #return
* #throws Throwable
*/
#Around("isAnnotated()")
public Object logTimeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object retVal = joinPoint.proceed();
stopWatch.stop();
logExecutionTime(joinPoint, stopWatch);
return retVal;
}
private void logExecutionTime(ProceedingJoinPoint joinPoint, StopWatch stopWatch) {
String logMessage = String.format(LOG_MESSAGE_FORMAT, joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName(), stopWatch.getTotalTimeMillis());
logger.info(logMessage.toString());
}
}
A class annotated with #Aspect isn't a #Component so if you have component scanning enabled it won't be picked up. If there is no Aspect in your context, there is nothing use for AOP.
To fix this you can do 1 of 3 things:
Put #Component next to the #Aspect
Define the #Aspect as a #Bean
Add an additional `#ComponentScan(includeFilter={#Filter(org.aspectj.lang.annotation.Aspect)}
Obviously option #1 is the easiest to do.
First I've tried to annotate some method method that throws an exception. It works fine but how can I make this work for all public methods in class annotated with #Loggable annotation?
You need to write a point cut that matches that. Something like the following should do the trick.
#Pointcut("public * ((#Loggable *)+).*(..)) && within(#Loggable *)")
together with
#Pointcut("#Loggable * *(..)")
Which will hit for annotated methods or public methods in annotated classes. This is inspired by the code from the AnnotationTransactionAspect from the Spring Framework.

Injecting into a Jersey Resource class

I did try going through the following links
How to wire in a collaborator into a Jersey resource?
and
Access external objects in Jersey Resource class
But still i am unable to find a working sample which shows how to inject into a Resource class.
I am not using Spring or a web container.
My Resource is
package resource;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
#Path("/something")
public class Resource
{
#MyResource
Integer foo = null;
private static String response = "SampleData from Resource";
public Resource()
{
System.out.println("...constructor called :" + foo);
}
#Path("/that")
#GET
#Produces("text/plain")
public String sendResponse()
{
return response + "\n";
}
}
My Provider is
package resource;
import javax.ws.rs.ext.Provider;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
#Provider
public class MyResourceProvider implements InjectableProvider<MyResource, Integer>
{
#Override
public ComponentScope getScope()
{
return ComponentScope.PerRequest;
}
#Override
public Injectable getInjectable(final ComponentContext arg0, final MyResource arg1, final Integer arg2)
{
return new Injectable<Object>()
{
#Override
public Object getValue()
{
return new Integer(99);
}
};
}
}
My EndpointPublisher is
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.MediaType;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.container.grizzly.GrizzlyWebContainerFactory;
class EndpointPublisher
{
public static void main(final String[] args)
{
final String address = "http://localhost:8080/";
final Map<String, String> config = new HashMap<String, String>();
config.put("com.sun.jersey.config.property.packages", "resource");
try
{
GrizzlyWebContainerFactory.create(address, config);
System.out.println("server started ....." + address);
callGet();
}
catch (final Exception e)
{
e.printStackTrace();
}
}
public static void callGet()
{
Client client = null;
ClientResponse response = null;
client = Client.create();
final WebResource resource =
client.resource("http://localhost:8080/something");
response = resource.path("that")
.accept(MediaType.TEXT_XML_TYPE, MediaType.APPLICATION_XML_TYPE)
.type(MediaType.TEXT_XML)
.get(ClientResponse.class);
System.out.println(">>>> " + response.getResponseDate());
}
}
My annotation being
#Retention(RetentionPolicy.RUNTIME)
public #interface MyResource
{}
But when i execute my EndpointPublisher i am unable to inject foo!!
Your InjectableProvider is not implemented correctly. The second type parameter should not be the type of the field you are trying to inject - instead it should be the context - either java.lang.reflect.Type class or com.sun.jersey.api.model.Parameter class. In your case, you would use Type. So, your InjectableProvider implementation should look as follows:
package resource;
import javax.ws.rs.ext.Provider;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import java.lang.reflect.Type;
#Provider
public class MyResourceProvider implements InjectableProvider<MyResource, Type> {
#Override
public ComponentScope getScope() {
return ComponentScope.PerRequest;
}
#Override
public Injectable getInjectable(final ComponentContext arg0, final MyResource arg1, final Type arg2) {
if (Integer.class.equals(arg2)) {
return new Injectable<Integer>() {
#Override
public Integer getValue() {
return new Integer(99);
}
};
} else {
return null;
}
}
}
There is a helper class for per-request injectable providers (PerRequestTypeInjectableProvider) as well as singleton injectable providers (SingletonTypeInjectableProvider), so you can further simplify it by inheriting from that:
package resource;
import javax.ws.rs.ext.Provider;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;
#Provider
public class MyResourceProvider extends PerRequestTypeInjectableProvider<MyResource, Integer> {
public MyResourceProvider() {
super(Integer.class);
}
#Override
public Injectable<Integer> getInjectable(ComponentContext ic, MyResource a) {
return new Injectable<Integer>() {
#Override
public Integer getValue() {
return new Integer(99);
}
};
}
}
Note that for these helper classes the second type parameter is the type of the field.
And one more thing - the injection happens after the constructor is called, so the constructor of your resource will still print out ...constructor called :null, but if you change your resource method to return foo, you'll see the response you'll get will be 99.
This solution works well and I wanted to share what I found to enable CDI on jersey resources.
Here is the simplest bean ever :
package fr.test;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
#RequestScoped
public class Test {
private int i;
#PostConstruct
public void create() {
i = 6;
}
public int getI() {
return i;
}
}
In your resource class, we just inject this bean, as we would do in a any normal context :
package fr.test;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
#Path("/login")
public class LoginApi {
#Inject
private Test test;
#GET
#Produces("text/plain")
public String getIt() {
return "Hi there!" + test;
}
}
And here is the key. We define a Jersey "InjectionProvider" which will be responsible of beans' resolution :
package fr.test;
import javax.inject.Inject;
import java.lang.reflect.Type;
import javax.ws.rs.ext.Provider;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import fr.xxxxxxxxxx.ApplicationBeans;
#Provider
public class InjectionProvider implements InjectableProvider<Inject, Type> {
public ComponentScope getScope() {
// CDI will handle scopes for us
return ComponentScope.Singleton;
}
#Override
public Injectable<?> getInjectable(ComponentContext context,
Inject injectAnno, Type t) {
if (!(t instanceof Class))
throw new RuntimeException("not injecting a class type ?");
Class<?> clazz = (Class<?>) t;
final Object instance = ApplicationBeans.get(clazz);
return new Injectable<Object>() {
public Object getValue() {
return instance;
}
};
}
}
InjectableProvider is typed with the kind of annotation we are handling, and the context type (here, normal java type)
ApplicationBeans is just a simple helper for bean resolution. Here is its content :
package fr.xxxxxxxxxx;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import fr.xxxxxxxxxxxxx.UnexpectedException;
/**
* Gives direct access to managed beans - Designed to be used from unmanaged code
*
* #author lgrignon
*
*/
#ApplicationScoped
public class ApplicationBeans
{
protected static ApplicationBeans instance;
#Inject
private BeanManager beanManager;
/**
* Gets instance
*
* #return Instance from managed environment
*/
public static ApplicationBeans instance()
{
if (instance == null)
{
BeanManager beanManager;
InitialContext ctx = null;
try
{
ctx = new InitialContext();
beanManager = (BeanManager)ctx.lookup("java:comp/BeanManager");
}catch(NamingException e)
{
try
{
beanManager = (BeanManager)ctx.lookup("java:app/BeanManager");
}catch(NamingException ne)
{
throw new UnexpectedException("Unable to obtain BeanManager.", ne);
}
}
instance = getBeanFromManager(beanManager, ApplicationBeans.class);
}
return instance;
}
/**
* Gets bean instance from context
*
* #param <T>
* Bean's type
* #param beanType
* Bean's type
* #param annotations
* Bean's annotations
* #return Bean instance or null if no
*/
public static <T> T get(final Class<T> beanType, Annotation... annotations)
{
return instance().getBean(beanType, annotations);
}
/**
* Gets bean instance from context
*
* #param <T>
* Bean's type
* #param beanType
* Bean's type
* #param annotations
* Bean's annotations
* #return Bean instance or null if no
*/
public <T> T getBean(final Class<T> beanType, Annotation... annotations)
{
return getBeanFromManager(beanManager, beanType, annotations);
}
#SuppressWarnings("unchecked")
private static <T> T getBeanFromManager(BeanManager beanManager, final Class<T> beanType, Annotation... annotations)
{
Set<Bean<?>> beans = beanManager.getBeans(beanType, annotations);
if (beans.size() > 1)
{
throw new UnexpectedException("Many bean declarations found for type %s (%s)", beanType.getSimpleName(), beansToString(beans));
}
if (beans.isEmpty())
{
throw new UnexpectedException("No bean declaration found for type %s", beanType.getSimpleName());
}
final Bean<T> bean = (Bean<T>)beans.iterator().next();
final CreationalContext<T> context = beanManager.createCreationalContext(bean);
return (T)beanManager.getReference(bean, beanType, context);
}
private static String beansToString(Collection<Bean<?>> beans)
{
String[] beansLabels = new String[beans.size()];
int i = 0;
for (final Bean<?> bean : beans)
{
beansLabels[i++] = bean.getName();
}
return Arrays.toString(beansLabels);
}
}
Hope this will help those who want to enable CDI injection in their Jersey resources.
Bye !

Categories

Resources