I have written one aspect to serialize the request arguments for APIs in Spring boot application, in DB as follows:
@Pointcut("within(com.tm.web.rest.*)") public void applicationResourcePointcut() { // Method is empty as this is just a Pointcut, the implementations are in the advices. } /** * Advice that logs when a method is returned. * * @param joinPoint join point for advice */ @AfterReturning(value = ("applicationResourcePointcut()"), returning = "returnValue") public void capturePayloadWhileReturning(JoinPoint joinPoint, Object returnValue) { CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); Map<String, Object> argumentNameValueMap = new HashMap<>(); if (codeSignature.getParameterNames() == null) { return mapper.writeValueAsString(argumentNameValueMap); } for (int i = 0; i < codeSignature.getParameterNames().length; i++) { String argumentName = codeSignature.getParameterNames()[i]; Object argumentValue = joinPoint.getArgs()[i]; argumentNameValueMap.put(argumentName, mapper.convertValue(argumentValue, Map.class)); } String s = mapper.writeValueAsString(argumentNameValueMap); }
The above code snippet is failing if we get HttpServletRequest
/ByteStream
as a request argument.
For example, for byte stream I am getting following exceptions:
java.lang.IllegalArgumentException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"])
For the request type of HttpServletRequest
, I am getting StackOverflow error.
Actually, I would like to avoid these types of arguments. But I am not able to figure out any approach to correctly handle this.
Could anyone please help here?
Advertisement
Answer
Joy, if you ask questions, try to provide a complete MCVE, do not keep the volunteers who want to help you guessing. In this case, you have problems with serialising data but neither did you mention which serialisation technology or tool you use nor is it recognisable from your code because the aspect advice uses an object mapper
without you showing how it is being declared. I do not understand why so many developers choose brevity over clarity.
After some googling on mapper.writeValueAsString(..)
I found out that probably you use Jackson. I am going to assume that this is true.
So one way to solve your problem is to just write a custom serialiser for the problematic classes, see this tutorial. Some serialisation exceptions might also be avoided by tweaking the mapper configuration.
The other way is to avoid serialising (or “json-ising”) those objects altogether and write some dummy value or the result of
toString()
to the database instead, whatever. Is this what you were asking about? Then you could- simply keep a static excludes list of classes in your aspect or
- build a dynamic list, using
try
/catch
blocks and adding classes for which Jackson is failing to serialise to the list, next time avoiding serialisation for the same class, or - just always use
try
/catch
, falling back totoString()
.
I think #1 would be nicer overall, but because your question was about AOP more than about Jackson (also according to the tags you selected), I am going to show you #2.3.
Further looking at your sample code, it looks a bit weird:
- For example, it would never compile like this due to the
return mapper.writeValueAsString(..)
statement in avoid
method. - You bind
returnValue
but never use it. - You call
codeSignature.getParameterNames()
in three different places, one of them inside a loop, instead of caching the value in a local variable. That should be simplified. - You could cast the signature to
MethodSignature
instead of the more generalCodeSignature
. Then you would have access to the method’s return type. Spring AOP does not support intercepting constructors anyway, only AspectJ does. Assuming you use Spring AOP, the only thing you can intercept are methods. - I do not understand why you call
mapper.convertValue(..)
upon each method parameter value, trying to convert it into aMap
. Why don’t you just usewriteValueAsString(..)
instead? - You check
getParameterNames()
fornull
, but it never returnsnull
, rather an empty array. So this check is not necessary. - Please also note that your whole idea of storing parameter names only works if the class is compiled with debug information. Otherwise there would not be any real parameter names, only surrogates like
arg0
,arg1
etc. So you rather want to be very sure that the code is compiled the right way before implementing the solution like this. - Calling
mapper.writeValueAsString(argumentNameValueMap)
on the map already containing JSON objects would lead to strings like"foo"
being enclosed in double quotes again like""foo""
, which probably is not what you want. Make sure you only serialise each object once.
Here is my MCVE:
Sample component:
package de.scrum_master.spring.q64782403; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; @Component public class MyComponent { public void doSomething() { System.out.println("Doing something"); } public int add(int a, int b) { System.out.println("Adding"); return a+b; } public void someRequest(HttpServletRequest request, String parameter) { System.out.println("Handling request"); } public void someByteStream(int index, ByteArrayInputStream stream) { System.out.println("Handling byte array input stream"); } public String concatenate(String a, String b) { System.out.println("Concatenating"); return a + " " + b; } }
Driver application:
package de.scrum_master.spring.q64782403; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; import java.io.ByteArrayInputStream; @SpringBootApplication public class Application { public static void main(String[] args) { try (ConfigurableApplicationContext context = SpringApplication.run(Application.class, args)) { doStuff(context); } } private static void doStuff(ConfigurableApplicationContext context) { MyComponent myComponent = context.getBean(MyComponent.class); myComponent.doSomething(); myComponent.add(4, 5); myComponent.someByteStream(11, new ByteArrayInputStream(new byte[1024])); myComponent.someRequest(new MockHttpServletRequest("GET", "/my/request"), "foo"); myComponent.concatenate("Hello", "world"); } }
Please note that for this dummy application I just use MockHttpServletRequest
, so if you want this to compile you need to add org.springframework:spring-test
as a compile dependency.
Aspect:
package de.scrum_master.spring.q64782403; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Component @Aspect public class SerialiserAspect { ObjectMapper mapper = new ObjectMapper(); @AfterReturning( value = "within(de.scrum_master.spring.q64782403..*)", returning = "returnValue" ) public void capturePayloadWhileReturning(JoinPoint joinPoint, Object returnValue) throws JsonProcessingException { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String[] argumentNames = signature.getParameterNames(); Object[] argumentValues = joinPoint.getArgs(); assert argumentNames.length == argumentValues.length; System.out.println(joinPoint); System.out.println(" Argument names = " + Arrays.deepToString(argumentNames)); System.out.println(" Argument types = " + Arrays.deepToString(signature.getParameterTypes())); System.out.println(" Argument values = " + Arrays.deepToString(argumentValues)); System.out.println(" Return type = " + signature.getReturnType()); System.out.println(" Return value = " + returnValue); Map<String, Object> arguments = new HashMap<>(); for (int i = 0; i < argumentNames.length; i++) { String argumentName = argumentNames[i]; Object argumentValue = argumentValues[i]; try { mapper.writeValueAsString(argumentValue); } catch (JsonProcessingException e) { argumentValue = argumentValue.toString(); System.out.println("Serialisation problem, falling back to toString():n " + e); } arguments.put(argumentName, argumentValue); } System.out.println(mapper.writeValueAsString(arguments)); } }
The first block of logging the joinpoint, arguments and return value to the console is just so as to help you see what the aspect is doing.
Console log:
2020-11-12 10:04:39.522 INFO 19704 --- [ main] d.s.spring.q64782403.Application : Started Application in 4.49 seconds (JVM running for 6.085) Doing something execution(void de.scrum_master.spring.q64782403.MyComponent.doSomething()) Argument names = [] Argument types = [] Argument values = [] Return type = void Return value = null {} Adding execution(int de.scrum_master.spring.q64782403.MyComponent.add(int,int)) Argument names = [a, b] Argument types = [int, int] Argument values = [4, 5] Return type = int Return value = 9 {"a":4,"b":5} Handling byte array input stream execution(void de.scrum_master.spring.q64782403.MyComponent.someByteStream(int,ByteArrayInputStream)) Argument names = [index, stream] Argument types = [int, class java.io.ByteArrayInputStream] Argument values = [11, java.io.ByteArrayInputStream@1e3ff233] Return type = void Return value = null Serialisation problem, falling back to toString(): com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) {"stream":"java.io.ByteArrayInputStream@1e3ff233","index":11} Handling request execution(void de.scrum_master.spring.q64782403.MyComponent.someRequest(HttpServletRequest,String)) Argument names = [request, parameter] Argument types = [interface javax.servlet.http.HttpServletRequest, class java.lang.String] Argument values = [org.springframework.mock.web.MockHttpServletRequest@9accff0, foo] Return type = void Return value = null Serialisation problem, falling back to toString(): com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.util.Collections$3 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.mock.web.MockHttpServletRequest["servletContext"]->org.springframework.mock.web.MockServletContext["servletNames"]) {"request":"org.springframework.mock.web.MockHttpServletRequest@9accff0","parameter":"foo"} Concatenating execution(String de.scrum_master.spring.q64782403.MyComponent.concatenate(String,String)) Argument names = [a, b] Argument types = [class java.lang.String, class java.lang.String] Argument values = [Hello, world] Return type = class java.lang.String Return value = Hello world {"a":"Hello","b":"world"}