在Spring Boot应用程序中序列化API请求参数的问题

问题描述

我已经写了一个方面,可以在DB的Spring Boot应用程序中序列化API的请求参数,如下所示:

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

如果我们将HttpServletRequest / ByteStream作为请求参数,上述代码段将失败。

例如,对于字节流,我遇到以下异常:

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"])
  

对于HttpServletRequest的请求类型,我收到StackOverflow错误。

实际上,我想避免这些类型的争论。但是我无法找出任何正确处理此问题的方法。

有人可以在这里帮忙吗?

解决方法

欢乐,如果您提出问题,请尝试提供完整的MCVE,不要让想帮助您进行猜测的志愿者留下。在这种情况下,您在序列化数据时会遇到问题,但是您既未提及所使用的序列化技术或工具,也未从代码中识别出该序列化技术或工具,因为方面建议使用了对象mapper,而没有显示其声明方式。我不明白为什么这么多开发人员选择简洁而不是清晰度。

在搜索mapper.writeValueAsString(..)之后,我发现您可能使用了Jackson。我要假设这是真的。

  1. 所以解决问题的一种方法是只为有问题的类编写自定义序列化程序,请参见this tutorial。通过调整映射器配置,还可以避免某些序列化异常。

  2. 另一种方法是避免完全序列化(或“ json-ising”)这些对象,而是将一些虚拟值或toString()的结果写入数据库,无论如何。这是您要问的吗?那你可以

    1. 仅保留您所处方面的静态排除类列表,或
    2. 使用try / catch块构建动态列表,并添加Jackson无法序列化的类到列表中,下次避免对同一类进行序列化,或者
    3. 始终使用try / catch,回到toString()

我认为#1总体而言会更好,但是由于您的问题是关于AOP而不是有关Jackson的问题(还根据您选择的标签),因此我将向您展示#2.3。

再看一下示例代码,看起来有点奇怪:

  • 例如,由于return mapper.writeValueAsString(..)方法中的void语句,它将永远不会像这样编译。
  • 您绑定了returnValue,但从不使用它。
  • 您可以在三个不同的位置调用codeSignature.getParameterNames(),而不是将值缓存在局部变量中,这三个位置之一是在循环中。应该简化。
  • 您可以将签名强制转换为MethodSignature,而不是更通用的CodeSignature。然后,您将可以访问该方法的返回类型。无论如何,Spring AOP不支持拦截构造函数,只有AspectJ支持。假设您使用的是Spring AOP,那么唯一可以拦截的就是方法。
  • 我不明白您为什么要在每个方法参数值上调用mapper.convertValue(..),并尝试将其转换为Map。为什么不只使用writeValueAsString(..)呢?
  • 您在getParameterNames()中检查了null,但是它从不返回null,而是一个空数组。因此,此检查不是必需的。
  • 还请注意,仅当使用调试信息编译该类时,您存储参数名称的整个想法才有用。否则,将没有任何实际的参数名称,只能使用诸如arg0arg1之类的代名词。因此,您宁愿非常确定在实现这种解决方案之前,应正确地编译代码。
  • 在已经包含JSON对象的地图上调用mapper.writeValueAsString(argumentNameValueMap)会导致像"foo"这样的字符串再次用"\"foo\""括在双引号中,这可能不是您想要的。确保只序列化每个对象一次。

这是我的MCVE

示例组件:

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

驱动程序应用程序:

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

请注意,对于该虚拟应用程序,我只使用MockHttpServletRequest,因此,如果要对其进行编译,则需要添加org.springframework:spring-test作为编译依赖项。

方面:

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

将连接点,参数和返回值记录到控制台的第一步是为了帮助您了解方面在做什么。

控制台日志:

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

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...