Spring AOP :- Passing method parameters dynamically to advice - spring

My ServiceImpl class contains multiple methods.I have one LoggingAspect and i want to dynamically pass the method parameters to an advice based on the method called in the ServiceImpl. how do i acheive this?In the below code my method has 4 parameters,but one of my other method has 5 parametres and the third has 3.So how do i pass parameters dynamically based on number of method parameters?
#Aspect
public class LoggingAspect {
#Before("allGenericAppServiceImplMethods(userid,galleryId,sid,imageName)")
public void LoggingAdvice(JoinPoint joinPoint,String userid,String galleryId,String sid,String imageName){
System.out.println("String values are "+userid+" "+sid+" "+galleryId+" "+imageName);
}
#Pointcut("execution(public * com.nrollup.service.impl.GenericAppServiceImpl.*(..)) && args(userid,galleryId,sid,imageName)")
public void allGenericAppServiceImplMethods(String userid,String galleryId,String sid,String imageName)
{
}
}

You can use the around advice for this. A code snippet for your reference:
public void myAdvice(final ProceedingJoinPoint joinPoint) throws Throwable
{
Object[] arguments = joinPoint.getArgs();
}
Once you have the arguments array, you can operate upon it as required.
You can use joinPoint.proceed() to continue with the function call.

Related

Using SPEL to call a method in the same class passing a variable taken from the actual method

I want to be able to call another method of my class by passing the value of a parameter of the calling method, so suppose:
public class MyClass{
#MySpelAnnotation("#this.otherMethod()") //how to I pass "arg" as parameter?
public void mainMethod(String arg){
...
}
public void otherMethod(String arg){
}
How can I be able to take arg from the first method and pass it to the second one?

Spring AOP pass String argument of a controller method

I have a controller class with one RequestMapping method which is taking String arguments. I want to pass this argument by using Spring AOP but its failing, I am getting null value when I am printing the value.
Tried with the below provided solution but its working with map but not with String.
Spring AOP pass argument of a controller method
#Controller
public class WelcomeController {
#Autowired
private FamilyService familyService;
#RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView welcomePage(String welcomeMessage) {
FamilyVO allFamilyMembers = familyService.getAllFamilyMembers();
ModelAndView modelAndView = new ModelAndView("Index", "family", allFamilyMembers);
List<String> familyMemberAges = new ArrayList<String>();
for (int i = 0; i <= 100; i++) {
familyMemberAges.add("" + i);
}
modelAndView.addObject("familyMemberAges", familyMemberAges);
System.out.println(welcomeMessage);
return modelAndView;
}
}
#Component
#Aspect
public class WelcomeControllerAspect {
#Before("execution(* com.kalavakuri.webmvc.web.controller.WelcomeController.welcomePage(..))")
public void beforeWelcomePage(JoinPoint joinPoint) {
joinPoint.getArgs()[0] = "Hellow";
System.out.println(joinPoint.getArgs().length);
System.out.println("Before welcomepage");
}
}
I am expecting the value "Hello" when I print it in Controller class but printing null.
A #Before advice is not meant to manipulate method parameters. In the example you linked to it only works because the argument is a mutable object, namely a Map. A String is immutable, though, you cannot edit it.
Having said that, what should you do? Use an #Around advice which was designed for that kind of thing. There you can decide how you want to proceed, e.g.
call the original method with original parameters,
call the original method with changed parameters,
do something before and/or after calling the original,
don't call the original method but return another result instead,
handle exceptions in the original method
or any mix of the above which makes sense (maybe you have multiple cases and if-else or switch-case).
I also suggest not to work directly on the Object[] of JoinPoint.getArgs() but to bind the relevant method parameter(s) to a named and type-safe advice parameter via args(). Try this:
package de.scrum_master.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
#Component
#Aspect
public class WelcomeControllerAspect {
#Around(
"execution(* com.kalavakuri.webmvc.web.controller.WelcomeController.welcomePage(..)) && " +
"args(welcomeMessage)"
)
public Object beforeWelcomePage(ProceedingJoinPoint joinPoint, String welcomeMessage) throws Throwable {
System.out.println(joinPoint + " -> " + welcomeMessage);
return joinPoint.proceed(new Object[] { "Hello AOP!" });
}
}
You should use an #Around advice instead of a #Before.
See this answer for more details.

Spring MVC RestController allow params with different names in methods

I am writing an API using Spring MVC and I am coming up with a problem allowing apps written in different languages to consume my API.
It turns out that the "Ruby users" like to have their params named in snake_case and our "Java users" like to have their param names in camel_case.
Is it possible to create my methods that allow param names to be named multiple ways, but mapped to the same method variable?
For instance... If I have a method that accepts a number of variables, of them there is mapped to a postal code. Could I write my method with a #RequestParam that accepts BOTH "postal_code" and "postalCode" and maps it to the same variable?
Neither JAX-RS #QueryParam nor Spring #RequestParam support your requirement i.e., mapping multiple request parameter names to the same variable.
I recommend not to do this as it will be very hard to support because of the confusion like which parameter is coming from which client.
But if you really wanted to handle this ((because you can't change the URL coming from 3rd parties, agreed long back), then the alternative is to make use of HandlerMethodArgumentResolver which helps in passing our own request argument (like #MyRequestParam) to the controller method like as shown in the below code:
Controller class:
#Controller
public class MyController {
#RequestMapping(value="/xyz")
public void train1(#MyRequestParam String postcode) {//custom method argument injected
//Add your code here
}
}
MyRequestParam :
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface MyRequestParam {
}
HandlerMethodArgumentResolver Impl class:
public class MyRequestParamWebArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
MyRequestParam myRequestParam =
parameter.getParameterAnnotation(MyRequestParam.class);
if(myRequestParam != null) {
HttpServletRequest request =
(HttpServletRequest) webRequest.getNativeRequest();
String myParamValueToBeSentToController = "";
//set the value from request.getParameter("postal_code")
//or request.getParameter("postalCode")
return myParamValueToBeSentToController;
}
return null;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.getParameterAnnotation(MyRequestParam.class) != null);
}
}
WebMvcConfigurerAdapter class:
#Configuration
class WebMvcContext extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new MyRequestParamWebArgumentResolver());
}
}
I think what you want to do is not allowed by Spring framework with the annotation RequestParam.
But if you can change the code or say to your third party to modify the calls i would suggest you 2 options
Option 1:
Use the #PathVariable property
#RequestMapping(value = "/postalcode/{postalCode}", method = RequestMethod.GET)
public ModelAndView yourMethod(#PathVariable("postalCode") String postalCode) {
//...your code
Here does not matter if the are calling your URL as:
http://domain/app/postalcode/E1-2ES
http://domain/app/postalcode/23580
Option 2:
Create 2 methods in your controller and use the same service
#RequestMapping(value = "/postalcode", method = RequestMethod.GET, params={"postalCode"})
public ModelAndView yourMethod(#RequestParam("postalCode") String postalCode) {
//...call the service
#RequestMapping(value = "/postalcode", method = RequestMethod.GET, params={"postal_code"})
public ModelAndView yourMethodClient2(#RequestParam("postal_code") String postalCode) {
//...call the service
If is possible, I would suggest you option 1 is much more scalable

Spring MVC data binding diff in javabean and #RequestParam boolean request parameter

Spring MVC support request parameter to javabean. But when I want to bind boolean,
It is still not work in javabean.
If the code in controller is:
public void test(#RequestParam(value="isCheck") boolean isCheck)
I can get the isCheck boolean value.
But when the code in contoller is:
public void test(TestVO testVO)
TestVO javabean:
public class TestVO {
private boolean isPrecheck;
public boolean isPrecheck() {
return isPrecheck;
}
public void setPrecheck(boolean isPrecheck) {
this.isPrecheck = isPrecheck;
}
}
I can not set the request parameter to this Javabean.
Anyone has ideas?
You can you the PropertiesEditor of the Spring.
http://static.springsource.org/spring/docs/3.2.x/javadoc-api/org/springframework/beans/propertyeditors/CustomBooleanEditor.html
Inside your controller create a method to set the editor
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Boolean.class, new CustomBooleanEditor(allowEmpty???));
}
This is due to boolean naming convention and associated ambiguity with its getter methods.
Rename your boolean from isPrecheck to precheck and it will work.

Adding multiple validators using initBinder

I'm adding a user validator using the initBinder method:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
Here is the UserValidator
public class UserValidator implements Validator {
public boolean supports(Class clazz) {
return User.class.equals(clazz);
}
public void validate(Object target, Errors errors) {
User u = (User) target;
// more code here
}
}
The validate method is getting properly called during the controller method call.
#RequestMapping(value = "/makePayment", method = RequestMethod.POST)
public String saveUserInformation(#Valid User user, BindingResult result, Model model){
// saving User here
// Preparing CustomerPayment object for the payment page.
CustomerPayment customerPayment = new CustomerPayment();
customerPayment.setPackageTb(packageTb);
model.addAttribute(customerPayment);
logger.debug("Redirecting to Payment page.");
return "registration/payment";
}
But while returning to the payment screen I'm getting this error:
java.lang.IllegalStateException: Invalid target for Validator [com.validator.UserValidator#710db357]: com.domain.CustomerPayment[ customerPaymentId=null ]
org.springframework.validation.DataBinder.setValidator(DataBinder.java:476)
com.web.UserRegistrationController.initBinder(UserRegistrationController.java:43)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
java.lang.reflect.Method.invoke(Method.java:597)
org.springframework.web.bind.annotation.support.HandlerMethodInvoker.initBinder(HandlerMethodInvoker.java:393)
org.springframework.web.bind.annotation.support.HandlerMethodInvoker.updateModelAttributes(HandlerMethodInvoker.java:222)
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:429)
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:414)
This might be because I'm returning a CustomerPayment and there is not validator defined for that.
I'm also not able to add multiple validators in initBinder method.
How can I fix this?
You need to set the value of the #InitBinder annotation to the name of the command you want it to validate. This tells Spring what to apply the binder to; without it, Spring will try to apply it to everything. This is why you're seeing that exception: Spring is trying to apply the binder - with your UserValidator - to a parameter of type CustomerPayment.
In your specific case, it looks like you need something like:
#InitBinder("user")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
To your second question, as Rigg802 explained, Spring does not support attaching multiple validators to a single command. You can, however, define multiple #InitBinder methods for different commands. So, for example, you could put the following in a single controller and validate your user and payment parameters:
#InitBinder("user")
protected void initUserBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
#InitBinder("customerPayment")
protected void initPaymentBinder(WebDataBinder binder) {
binder.setValidator(new CustomerPaymentValidator());
}
It's a bit tricky to do, 1 controller has only 1 validator on 1 command object.
you need to create a "Composite Validator" that will get all the validators and run them seperately.
Here is a tutorial that explains how to do it: using multiple validators
You can add multiple validators by iterating over all org.springframework.validation.Validator in an ApplicationContext and set up suitable ones in #InitBinder for each request.
#InitBinder
public void setUpValidators(WebDataBinder webDataBinder) {
for (Validator validator : validators) {
if (validator.supports(webDataBinder.getTarget().getClass())
&& !validator.getClass().getName().contains("org.springframework"))
webDataBinder.addValidators(validator);
}
}
See my project for examples and simple benchmarks. https://github.com/LyashenkoGS/spring-mvc-and-jms-validation-POC/tree/benchamark
I do not see a reason why Spring does not filter out all validators which are not applicable to the current entity by default which forces to use things like CompoundValidator described by #Rigg802.
InitBinder allows you to specify name only which give you some control but not full control over how and when to apply your custom validator. Which from my perspective is not enough.
Another thing you can do is to perform check yourself and add validator to binder only if it is actually necessary, since binder itself has binding context information.
For example if you want to add a new validator which will work with your User object in addition to built-in validators you can write something like this:
#InitBinder
protected void initBinder(WebDataBinder binder) {
Optional.ofNullable(binder.getTarget())
.filter((notNullBinder) -> User.class.equals(notNullBinder.getClass()))
.ifPresent(o -> binder.addValidators(new UserValidator()));
}
There is a simple hack, always return true in supports method, and delegate the class checking to validate. Then basically you can add multiple validator in the initBinder without issue.
#Component
public class MerchantRegisterValidator implements Validator {
#Autowired
private MerchantUserService merchantUserService;
#Autowired
private MerchantCompanyService merchantCompanyService;
#Override
public boolean supports(Class<?> clazz) {
return true; // always true
}
#Override
public void validate(Object target, Errors errors) {
if (!RegisterForm.getClass().equals(target.getClass()))
return; // do checking here.
RegisterForm registerForm = (RegisterForm) target;
MerchantUser merchantUser = merchantUserService.getUserByEmail(registerForm.getUserEmail());
if (merchantUser != null) {
errors.reject("xxx");
}
MerchantCompany merchantCompany = merchantCompanyService.getByRegno(registerForm.getRegno());
if (merchantCompany != null) {
errors.reject("xxx");
}
}
}
Multiple validator on one command is supported with Spring MVC 4.x now. You could use this snippet code:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new UserValidator(), new CustomerPaymentValidator());
}
The safest way is to add a generic validator handling that Controller:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new GenericControllerOneValidator());
}
Then, in the generic validator you can support multiple request body models and based of the instance of the object, you can invoke the appropriate validator:
public class GenericValidator implements Validator {
#Override
public boolean supports(Class<?> aClass) {
return ModelRequestOne.class.equals(aClass)
|| ModelRequestTwo.class.equals(aClass);
}
#Override
public void validate(Object body, Errors errors) {
if (body instanceof ModelRequestOne) {
ValidationUtils.invokeValidator(new ModelRequestOneValidator(), body, errors);
}
if (body instanceof ModelRequestTwo) {
ValidationUtils.invokeValidator(new ModelRequestTwoValidator(), body, errors);
}
}
}
Then you add your custom validations inside for each model validator implementatios. ModeRequestOneValidator and ModeRequestTwoValidator still need to implement the Validator interface of org.springframework.validation
Also, do not forget to use #Valid ModeRequestOne and #Valid ModeRequestTwo inside the controllers method call.
One addition to Annabelle's answer:
If controller has this method parameter and you want to validate that one specifically
#RequestMapping(value = "/users", method = RequestMethod.POST)
public String findUsers(UserRequest request){..}
Then the binding should be lower case of the class name (but just the first letter, and not everything else)
#InitBinder("userRequest")
protected void initUserBinder(WebDataBinder binder) {
binder.setValidator(new YourValidator());
}
Declare request as
(... , Model model,HttpServletRequest request)
and change
model.addAttribute(customerPayment);
to
request.setAttribute("customerPayment",customerPayment);

Resources