java.lang.StackOverflowError converting HttpServletRequest to JSON

心已入冬 提交于 2020-01-07 03:04:52

问题


In my spring rest app, I log every api endpoints arguments in aspect.

@Aspect
@Component
public class EndpointsAspect {

   @Around("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
   public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {

      Map<String, Object> log = new HashMap<>();


      String[] parameterNames =  methodSignature.getParameterNames();
      Object[] parameterValues = joinPoint.getArgs();

      Map<String, Object> arguments = new HashMap<>();

        for (int i = 0; i < parameterNames.length; i++) {
            arguments.put(parameterNames[i], parameterValues[i]);
        }
        log.put("Method arguments", arguments);

        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String json = gson.toJson(log);

        ...

        Object retVal = joinPoint.proceed();
   }

}

It works fine, until one of argument of adviced method has argument of type HttpServletRequest

    @RequestMapping("/info")
    public String index(HttpServletRequest request) {  
        return "Info";
    }

In this case java.lang.StackOverflowError is raised.

I know that this is somehow related to HttpServlterRequest variable (maybe some infinitive loop), but how to solve this problem?

Ho to limit gson depth?

I've looked at some solutions (annotate fields or classes that should be converted to json with some annotations), but it's not suitable for me, this should be universal solutions for all classes and cases (I acn't, for instance, annotate HttpServletRequest with some annotations, or include it to gson exclusion strategy, because who nows wich classes will be converted to json), I need log data as json, but logger shouldn't be application fault point because of serialization issue.

Thank you.


回答1:


I've found some solution and answer my question.

For json serializer I use flexjson library (http://flexjson.sourceforge.net/). It supports serialization of classes which have bidirectional relationship without any annotations and other extra work.

In case of HttpServletRequest, I wrap serialization in try catch block and log that, for example, argument "request" could not be serialized.

@Around("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {

    MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();

    Map<String, Object> log = new HashMap<>();
    log.put("Method", joinPoint.getSignature().toString());

    String[] parameterNames =  methodSignature.getParameterNames();
    Object[] parameterValues = joinPoint.getArgs();

    Map<String, Object> arguments = new HashMap<>();
    for (int i = 0; i < parameterNames.length; i++) {
      // Check if argument can be serialized and put arguments to argument list if possible. For example, HttpServletRequest cannot be serialized
      try {
         JSONSerializer serializer = new JSONSerializer();
         String json = serializer.prettyPrint(true).deepSerialize(parameterValues[i]);
         arguments.put(parameterNames[i], parameterValues[i]);
      }
      catch (Exception serializerException) {
         arguments.put(parameterNames[i], "Couldn't serialize argument. "+serializerException.getMessage());
      }    
    }
   log.put("Method arguments", arguments);

   ...

   try {
     JSONSerializer serializer = new JSONSerializer();
     String json = serializer.prettyPrint(true).deepSerialize(log);
     logger.info(json);
   }
   catch (Exception e) {
     logger.error("Could not serialize data. "+e.getMessage());
   }       

   ... 
}

And log looks something like this:

{
    "Path": "/v1/users/1",
    "Http Status": "200 OK",
    "Method arguments": {
        "request": "Couldn't serialize argument. Error trying to deepSerialize",
        "id": 1
    },
    "Headers": {
        "accept-language": "en-US,en;q=0.8",
        "cookie": "lbannounce=; ServiceTransactionSuccess=false; ServiceTransactionAmount=false; ServiceTransactionWidgetType=false; ServiceTransactionDomain=false; _ga=GA1.1.1011235846.1448355706",
        "host": "localhost:8080",
        "connection": "keep-alive",
        "accept-encoding": "gzip, deflate, sdch",
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "user-agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36"
    },
    "Return Value": {
        "class": "api.domain.User",           
        "email": "user@mail.com",
        "id": 1,         
        "username": "user111"
    },
    "Ip": "0:0:0:0:0:0:0:1",
    "Http Method": "GET",
    "Method": "User api.web.UserController.user(int,HttpServletRequest)"
} 



回答2:


I don't know how important is for you to go deeper en each parameter value for logging, but one workaround could be to add the string representation for each parameter instead of the object:

for (int i = 0; i < parameterNames.length; i++) {
    arguments.put(parameterNames[i], parameterValues[i].toString());
}

In case of HttpServletRequest you probably get an awful log, but it's a log! for your own classes you can overwrite toString() method to define it as you want (not a bad practice by the way).

If it doesn't work for you, you can add a hack for external classes (not inside you package root) for example:

for (int i = 0; i < parameterNames.length; i++) {
    Object value = parameterValues[i];
    if (!"com.mypackage.foo.bar".equals(value.getClass().getPackage().getName()) {
      value = value.toString();
    }
    arguments.put(parameterNames[i], value);
}

It's a hack but it could help maybe.

But if you don't like that easy solution, the pretty one could be to use Gson serializers (as expianed here: https://sites.google.com/site/gson/gson-user-guide#TOC-Using-Gson), and you are responsible to implement hot an HttpServletRequest will be serialized

GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(HttpServletRequest.class, new MyCustonSerializer());

... 

class MyCustonSerializer implements JsonSerializer<HttpServletRequest> {
    public JsonElement serialize(HttpServletRequest src, Type typeOfSrc, JsonSerializationContext context) {
       return new JsonPrimitive(src.toString());
  }  
}

EDIT: - add a full example -

public class GsonTest {

  public static void main(String[] args) {
      GsonBuilder gb = new GsonBuilder();
      gb.registerTypeAdapter(Integer.class, new MyCustonHttpServletRequestSerializer());
      //gb.registerTypeAdapter(Object.class, null);
      Map<String, Object> map = new HashMap<String, Object>();
      map.put("a", "String a");
      map.put("b", new Integer(3));
      map.put("c", new Person("John", "Doe"));
      Gson gson = gb.setPrettyPrinting().create();
      String json = gson.toJson(map);
      System.out.println(json);
  }  
}

class MyCustonHttpServletRequestSerializer implements JsonSerializer<Integer> {

  @Override
  public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) {
    return new JsonPrimitive("Number " + src);
  }
}

class Person {

  private String name;
  private String lastName;

  public Person(String name, String lastName) {
    this.name = name;
    this.lastName = lastName;
  }
}

and output will be

{
  "b": "Number 3",
  "c": {
    "name": "John",
    "lastName": "Doe"
  },
  "a": "String a"
}


来源:https://stackoverflow.com/questions/33900388/java-lang-stackoverflowerror-converting-httpservletrequest-to-json

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!