【问题标题】:java.lang.StackOverflowError converting HttpServletRequest to JSONjava.lang.StackOverflowError 将 HttpServletRequest 转换为 JSON
【发布时间】:2015-11-24 17:44:59
【问题描述】:

在我的 spring rest 应用程序中,我在方面记录每个 api 端点参数。

@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();
   }

}

它工作正常,直到建议方法的参数之一具有 HttpServletRequest 类型的参数

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

在这种情况下会引发 java.lang.StackOverflowError。

我知道这在某种程度上与 HttpServlterRequest 变量有关(可能是一些不定式循环),但是如何解决这个问题呢?

如何限制 gson 深度?

我查看了一些解决方案(使用一些注释来注释应该转换为 json 的字段或类),但它不适合我,这应该是所有类和案例的通用解决方案(我不能,因为例如,用一些注释对 HttpServletRequest 进行注释,或者将其包含在 gson 排除策略中,因为现在谁的类将被转换为 json),我需要将日志数据作为 json,但由于序列化问题,记录器不应该是应用程序故障点。

谢谢。

【问题讨论】:

    标签: java json spring gson


    【解决方案1】:

    我找到了一些解决方案并回答了我的问题。

    对于 json 序列化程序,我使用 flexjson 库 (http://flexjson.sourceforge.net/)。它支持对具有双向关系的类进行序列化,无需任何注释和其他额外工作。

    在 HttpServletRequest 的情况下,我将序列化包装在 try catch 块中并记录例如参数“request”无法序列化。

    @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());
       }       
    
       ... 
    }
    

    日志看起来像这样:

    {
        "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】:

      我不知道深入了解每个参数值对日志记录的重要性,但一种解决方法可能是为每个参数而不是对象添加字符串表示:

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

      如果是HttpServletRequest,你可能会得到一个糟糕的日志,但它是一个日志!对于您自己的类,您可以覆盖 toString() 方法以根据需要定义它(顺便说一句,这不是一个坏习惯)。

      如果它不适合您,您可以为外部类添加 hack(不在您的包根目录内) 例如:

      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);
      }
      

      这是一个 hack,但它可能会有所帮助。

      但是,如果您不喜欢这种简单的解决方案,那么漂亮的解决方案可能是使用 Gson 序列化程序(如本文所述:https://sites.google.com/site/gson/gson-user-guide#TOC-Using-Gson),并且您负责实现热的 HttpServletRequest 将被序列化

      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());
        }  
      }
      

      编辑:-添加完整示例-

      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;
        }
      }
      

      输出将是

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

      【讨论】:

      • 谢谢热尔瓦西奥。但也有一些问题。 1) (toString()) 我经常在端点方法中传递一些对象作为参数,例如在发布请求的情况下,我在端点方法中传递了用户对象,因此我看不到用户对象字段。 2)(黑客外部类)。在这种情况下,我可以拥有具有循环引用的对象(例如 JPA 实体 User->Company、Company->User),在这种情况下 gson 将引发异常。 3)(使用 Gson 序列化程序)。是的,我可以将它用于 HttpServletRequests,但是我应该为所有可能的类实现它(这是不可能的)。还有其他解决方案吗?)
      • 对于最后一个选项 3),“我应该为所有可能的类实现它” 是什么意思?您只注册要覆盖的类型,在您的情况下,HttpServletRequest。其他类型将默认工作。
      • 看看我添加到答案中的示例。我认为自定义序列化程序对你来说应该足够了
      • “我应该为所有可能的类实现它”我的意思是,例如,如果在某些端点方法中我将在未来传递 AnotherHttpServletRequest 类变量怎么办?与 HttpServletRequest 有同样的问题吗?如果我忘记为这个类实现序列化怎么办?在端点方法中,会有很多 JPA 实体,它们有循环引用,那么我应该为每个实体实现自定义序列化。我认为这不是很好的解决方案。可能还有其他一些具有深度限制的 json 转换器?
      猜你喜欢
      • 2015-12-28
      • 1970-01-01
      • 1970-01-01
      • 2018-05-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多