Issues in serializing API request argument in Spring boot application

Tags: , , , ,



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?

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.

  1. 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.

  2. 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

    1. simply keep a static excludes list of classes in your aspect or
    2. 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
    3. just always use try/catch, falling back to toString().

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 a void 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 general CodeSignature. 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 a Map. Why don’t you just use writeValueAsString(..) instead?
  • You check getParameterNames() for null, but it never returns null, 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"}


Source: stackoverflow