In the Spring Statemachine reference doc is this sample code:
#WithStateMachine
static class Bean1 {
#OnTransition(source = "S1", target = "S2")
public void fromS1ToS2() {
}
}
Is it possible to access the StateContext object from a method annotated with #OnTransition? Perhaps I don't understand the correct use of the annotation...I thought it could be used in a similar manner as an Action, where I could access data stored in the ExtendedState.
It seems that I've totally forget to add this specific information on our docs. We can't access StateContext but event headers and ExtendedState are available.
There's a unit test for this in MethodAnnotationTests.
Short story is that processor handling method calling detects argument types ExtendedState and Map if it's annotated with #EventHeaders. I've also been thinking to support StateContext in a same way via method arguments but haven't yet got that far.
#WithStateMachine
public class Bean2 {
#OnTransition(source = "S1", target = "S2")
public void method1(#EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
}
}
I'll also get docs sorted out for this, thx for pointing this out!
Their documentation says:
A method annotated with #OnTransition may accept a parameter of type
ExtendedState, Map if map argument itself is annotated with
EventHeaders, StateMachine, Message or Exception.
https://docs.spring.io/spring-statemachine/docs/current/api/org/springframework/statemachine/annotation/OnTransition.html
Related
I have methods annotated as follows:
#Action(actionType=<..>)
public void doSomeAction() {
...
}
Now, I would like to invoke one of such annotated methods with given actionType from another flow in another class.
In order to do this, I found the approaches as mentioned in the following SO posts:
How to run all methods with a given annotation?
Java seek a method with specific annotation and its annotation element
But in some cases, such methods may accept arguments as follows:
#Action(actionType='SEND_NOTIFICATION')
public void sendReport(String email) {
...
}
In this regard, I am not able to understand, how to check if the annotated method has any params and invoke such methods with required params.
I am trying to implement custom annotation and aspect which will insert path variable into request body before validation.
For now it looks like this...
#Aspect
#Component
public class AddParameterToFormAspect {
#Before("#annotation(addParameterToForm)")
public void addParameterToForm(JoinPoint joinPoint, AddParameterToForm addParameterToForm) {
String form = addParameterToForm.form();
String pathVariable = addParameterToForm.pathVariable();
CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature();
List<String> methodParamNames = Arrays.asList(methodSignature.getParameterNames());
int formIndex = 0;
int pathVariableIndex = 0;
for(String s : methodSignature.getParameterNames()) {
if(s.equals(form)) {
formIndex = methodParamNames.indexOf(s);
}
if(s.equals(pathVariable)) {
pathVariableIndex = methodParamNames.indexOf(s);
}
}
Object[] methodArgs = joinPoint.getArgs();
Object formObject = methodArgs[formIndex];
Field pathVariableObject;
try {
pathVariableObject = formObject.getClass().getDeclaredField(pathVariable);
pathVariableObject.setAccessible(true);
pathVariableObject.set(formObject, methodArgs[pathVariableIndex]);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
Controller example of working annotation...
#PostMapping("/test/{username}")
#AddParameterToForm(pathVariable = "username", form = "user")
public String test(#PathVariable String username, #RequestBody User user) {
return user.getUsername();
}
Controller example of validation not working...
#PostMapping("/{domainCode}")
#AddParameterToForm(pathVariable = "domainCode", form = "userAddForm")
public ResponseEntity<UserDto> saveUserForDomain(#PathVariable(name="domainCode") String domainCode, #RequestBody #Valid final UserAddForm userAddForm, BindingResult results) {...}
Adding path variable to form works but it seems #Valid no longer works, problem is probably in join point expression... How can I make it to do advice before validation and then validate?
Changing method parameters in a #Before advice is not meant to work. You should use an #Around advice in order to change parameters before calling thisJoinPoint.proceed(). This is because when calling thisJoinPoint.getArgs() you get copies of primitive type parameters, you cannot manipulate the originals in a before-advice. You are lucky that you want to manipulate object types in this case, so that is the reason it works. Using an around-advice would enable you to pass completely new arguments to a method or just manipulate the original objects, you are free to choose.
Furthermore, you should - whenever possible - use args() in order to bind your method arguments of interest to advice parameters in order to be able to interact with them in a non-cryptic and type-safe manner. Creating a local variable and assigning some value to it will not influence the method parameter of the same type at all. Why should it?
Feel free to ask follow-up questions if this explanation is not comprehensive enough for you. Then I could add some sample code for you, too.
Update after question edit:
After having inspected you code a bit more closely and in addition to my remarks earlier today in my comments under your question, disregarding the content of your aspect code, your actual problem is that the validation check cause by #Valid annotations is performed before the method is executed. I.e. what is validated is not the state after the aspect has done its job (populate member fields in your target objects) but the state before the aspect runs. It is actually the same problem discussed in this question, see also M. Deinum's and my suggestions how to solve it:
Maybe you want to try full AspectJ via LTW (load-time weaving) and see if a call() pointcut instead of the implicit execution() pointcut used by Spring AOP solves the problem. You would weave into the calling code (method calls) instead of the callee (method execution) itself. Chances are, that this happens before validation is performed.
A more Spring-like way to solve it is to use a Spring interceptor (M. Deinum mentions HandlerInterceptor) instead of an aspect. There is also a link to an example by someone else.
Having said that, I still recommend to refactor your code so as not to use reflection and matching strings on method parameter names or class member names. I think you could also get rid of your custom annotation by matching your pointcut on methods parameters with #RequestBody and #PathVariable annotations.
I have several APIs which retain a parameter "feature" from the url (path param). To avoid retrieving it in each method endpoint (eg.)
#GET
public void findAll(#PathParam("feature") String feature);
am trying to implement AOP using AspectJ.
Following is the implementation of the Aspect
#Aspect
public class FeatureAOP {
#Pointcut("execution(* x.y.z.rest.ModifiersFacadeWrapper.*(..)) && args(feature)")
public void pointCut(String feature) {
}
#Before("x.y.z.rest.aop.FeatureAOP.pointCut(feature)")
public void parseParams(JoinPoint jp, String feature) {
Object[] x = jp.getArgs();
System.out.println("Feature: " + feature);
}
}
The above method gives me the value of "feature" in the Aspect class but if I change the method findAll to following signature, it doesn't works.
#GET
public void findAll();
What I understand is the control is transferred to the Aspect after the parameters are resolved and removing it from the method definition is failing it.
Doing so, thus takes me to the same point where I have to define all method endpoints with the parameter in its signature. I would like to know if there is a way I can get the PathParams in the Aspect class without having to define my methods with the designated parameters.
I think you could probably do it by putting the resolved params in a globally accessible data structure (e.g. a Singleton having some sort of Map or Set), but
I wouldn't recommend that kind of approach. I don't know why you don't like having all the params in your method signatures, but that is the intended way of declaring rest services, e.g.
#GET
#Path("{feature}")
#Produces("text/plain")
public String getFeature(#PathParam("feature") String feature) {
return feature;
}
This way you don't have to write any code for retrieving the params, the rest library you are using (be it Jersey or a different one) will just do everything for you.
I want to provide annotations with some values generated by some methods.
I tried this so far:
public #interface MyInterface {
String aString();
}
#MyInterface(aString = MyClass.GENERIC_GENERATED_NAME)
public class MyClass {
static final String GENERIC_GENERATED_NAME = MyClass.generateName(MyClass.class);
public static final String generateName(final Class<?> c) {
return c.getClass().getName();
}
}
Thought GENERIC_GENERATED_NAME is static final, it complains that
The value for annotation attribute MyInterface.aString must be a constant expression
So how to achieve this ?
There is no way to dynamically generate a string used in an annotation. The compiler evaluates annotation metadata for RetentionPolicy.RUNTIME annotations at compile time, but GENERIC_GENERATED_NAME isn't known until runtime. And you can't use generated values for annotations that are RetentionPolicy.SOURCE because they are discarded after compile time, so those generated values would never be known.
The solution is to use an annotated method instead. Call that method (with reflection) to get the dynamic value.
From the user's perspective we'd have:
#MyInterface
public class MyClass {
#MyName
public String generateName() {
return MyClass.class.getName();
}
}
The annotation itself would be defined as
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface #MyName {
}
Implementing the lookup for both of these annotations is rather straight-forward.
// as looked up by #MyInterface
Class<?> clazz;
Method[] methods = clazz.getDeclaredMethods();
if (methods.length != 1) {
// error
}
Method method = methods[0];
if (!method.isAnnotationPresent(MyName.class)) {
// error as well
}
// This works if the class has a public empty constructor
// (otherwise, get constructor & use setAccessible(true))
Object instance = clazz.newInstance();
// the dynamic value is here:
String name = (String) method.invoke(instance);
There is no way to modify the properties of an annotation dynamically like others said. Still if you want to achieve that, there are two ways to do this.
Assign an expression to the property in the annotation and process that expression whenever you retrieve the annotation. In your case your annotation can be
#MyInterface(aString = "objectA.doSomething(args1, args2)")
When you read that, you can process the string and make the method invocation and retrieve the value. Spring does that by SPEL (Spring expression language). This is resource intensive and the cpu cycles are wasted every time we want to process the expression. If you are using spring, you can hook in a beanPostProcessor and process the expression once and store the result somewhere. (Either a global properties object or in a map which can be retrieved anywhere).
This is a hacky way of doing what we want. Java stores a private variable which maintains a map of annotations on the class/field/method. You can use reflection and get hold of that map. So while processing the annotation for the first time, we resolve the expression and find the actual value. Then we create an annotation object of the required type. We can put the newly created annotation with the actual value (which is constant) on the property of the annotation and override the actual annotation in the retrieved map.
The way jdk stores the annotation map is java version dependent and is not reliable since it is not exposed for use (it is private).
You can find a reference implementation here.
https://rationaleemotions.wordpress.com/2016/05/27/changing-annotation-values-at-runtime/
P.S: I haven't tried and tested the second method.
We're building some kind of automatic self describing REST service (live doc generation). For this we have a controller method that looks for all controller beans and fetches also the requestmapping information to display them in a nice friendly html page.
For this we use the MetadataReader (created via CachingMetadataReaderFactory) to fetch the Metadata of the class.
When we get the MethodMetaData for the public methods we find the RequestMapping annotation along with produces and value parameters, but the method field is always an empty array, although we have it configured in the source code and the mapping works. So the information should be somewhere. This is quite puzzling! :-)
EDIT: method field is empty=> The RequestMapping Annotation has a method field, which is an array of RequestMethod objects. If you try to read that from the MethodMetaData instance it's an empty array. Example: metadata.getAnnotationAttributes(RequestMapping.class.getName()).get("method")
I tried to find the reason in the spring framework source code, but haven't found the reason so far...
Any ideas?
FYI: We're using Spring 3.1
I created a little sample project out of my own curiosity and fiddled a little with the MetadataReader provided by Spring. For the demo I created a very simple controller which looked like this:
#Controller
public class SomeAnnotatedController {
#RequestMapping(method = {RequestMethod.GET}, value = "/someUrl")
public void someMethod() {
// do something later
}
}
I was not able to extract the correct information from the annotation using the Spring MetadataReader.
#Test
public void shouldReturnMethodArrayWithSpringMetadataReader() throws Exception {
MetadataReader metadataReader = new CachingMetadataReaderFactory().getMetadataReader(SomeAnnotatedController.class.getName());
Set<MethodMetadata> annotatedMethods = metadataReader.getAnnotationMetadata().getAnnotatedMethods(RequestMapping.class.getName());
assertEquals(1, annotatedMethods.size());
MethodMetadata methodMetadata = annotatedMethods.iterator().next();
assertEquals("someMethod", methodMetadata.getMethodName());
Map<String, Object> annotationAttributes = methodMetadata.getAnnotationAttributes(RequestMapping.class.getName());
assertTrue(annotationAttributes.containsKey("method"));
RequestMethod[] methodAttribute = (RequestMethod[]) annotationAttributes.get("method");
assertEquals(1, methodAttribute.length);
}
Running this test fails in the last line and tells you that this is an empty array...
java.lang.AssertionError:
Expected :1
Actual :0
Doing the same with native Java feels a little bit easier and returns the correct information.
#Test
public void shouldReturnMethodArrayWithPlainJava() throws Exception {
Method method = SomeAnnotatedController.class.getDeclaredMethod("someMethod");
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
assertEquals(1, annotation.method().length);
assertEquals(RequestMethod.GET, annotation.method()[0]);
}
So I am sorry to tell you that I did not find a solution to the problem but maybe the sample project or the documented alternative based on plain java might help.
This is not a direct answer to what you have asked for, but is a very good way to do self-document the REST API's. Use the endpoint documentation controller described by Rossen Stoyanchev here in his github location: https://github.com/rstoyanchev/spring-mvc-31-demo.git
to summarize, your controller would look something like this:
#Controller
public class EndpointDocController {
private final RequestMappingHandlerMapping handlerMapping;
#Autowired
public EndpointDocController(RequestMappingHandlerMapping handlerMapping) {
this.handlerMapping = handlerMapping;
}
#RequestMapping(value="/endpointdoc", method=RequestMethod.GET)
public void show(Model model) {
model.addAttribute("handlerMethods", this.handlerMapping.getHandlerMethods());
}
}
and your jsp would refer to the attributes for method, produces, consumes, method signature this way, asssuming the hm is a handler method:
Patterns:${hm.key.patternsCondition.patterns}
Method: ${hm.key.methodsCondition.methods}
Method signature: ${hm.value}
Consumes: ${hm.key.consumesCondition.expressions}
Produces: ${hm.key.producesCondition.expressions}