自定义 Spring Aspect 记录器导致异常

Custom Spring Aspect logger cause exception

提问人:ogbozoyan 提问时间:7/1/2023 最后编辑:kriegaexogbozoyan 更新时间:7/3/2023 访问量:163

问:

我编写了基于spring aop的系统记录器的自定义实现,一切正常,但是一段时间后,我开始收到像文件中一样的错误,这让我大吃一惊,绝对令人困惑的错误

java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "scheme" is null
  at org.apache.catalina.util.RequestUtil.getRequestURL(RequestUtil.java:51)
  at org.apache.catalina.connector.Request.getRequestURL(Request.java:2455)
  at org.apache.catalina.connector.RequestFacade.getRequestURL(RequestFacade.java:880)
  at javax.servlet.http.HttpServletRequestWrapper.getRequestURL(HttpServletRequestWrapper.java:226)
  at javax.servlet.http.HttpServletRequestWrapper.getRequestURL(HttpServletRequestWrapper.java:226)
  at javax.servlet.http.HttpServletRequestWrapper.getRequestURL(HttpServletRequestWrapper.java:226)
  at javax.servlet.http.HttpServletRequestWrapper.getRequestURL(HttpServletRequestWrapper.java:226)
  at com.example.pabp_business_logic.aspect.LoggingAspect.before(LoggingAspect.java:86)
  at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
  at java.base/java.lang.reflect.Method.invoke(Method.java:577)
  at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634)
  at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:617)
  at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:44)
  at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:57)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
  at org.springframework.aop.framework.CglibAopProxyCglibMethodInvocation.proceed(CglibAopProxy.java:763) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxyCglibMethodInvocation.proceed(CglibAopProxy.java:763)
  at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
  at com.example.pabp_business_logic.controller.pp.PpRSrcControllerEnhancerBySpringCGLIBca261473.getPage(<generated>)
  at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
  at java.base/java.lang.reflect.Method.invoke(Method.java:577)

,@ToLogger放置在控制器上的注释,导致当放置在服务方法上时,它会堆叠事务,甚至传播也无济于事,我不知道为什么在抛出此异常后它开始重试 Before 方法,它填充了 null 字段
微服务架构在 k8s 中运行,负载均衡器 idk 中的 mb 问题

/**
 * The LoggingAspect class is an aspect that provides logging functionality for
 * annotated methods. It captures the request and response data, logs the method
 * execution details, and saves the log entity to the database.
 *
 * @author ogbozoyan
 * @date 06.04.2023
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ToLogger {
  /**
   * Specifies the action associated with the annotated method.
   * Defaults to {@link ActionEnum#UNDEFINED}.
   *
   * @return the action associated with the method
   */
  ActionEnum action() default ActionEnum.UNDEFINED;

  /**
   * Specifies the action domain associated with the annotated method.
   * Defaults to {@link ActionDomainEnum#TEST}.
   *
   * @return the action domain associated with the method
   */
  ActionDomainEnum actionDomain() default ActionDomainEnum.UNDEFINED;

  /**
   * Specifies the HTTP method associated with the annotated method.
   * Defaults to {@link HttpMethodEnum#UNDEFINED}.
   *
   * @return the HTTP method associated with the method
   */
  HttpMethodEnum httpMethod() default HttpMethodEnum.UNDEFINED;

  /**
   * Indicates whether the response should be included in the log.
   * If set to {@code true}, the response will be set in the
   * {@code responseDataAfterChange} field of the log entity.
   * Defaults to {@code false}.
   *
   * @return {@code true} if the response should be included in the log,
   * {@code false} otherwise
   */
  boolean returnResponse() default false;

  /**
   * Indicates whether the log should be saved in the database.
   * If set to {@code true}, the log entity will be saved in the database.
   * If an exception is caught in the method, the log will be saved regardless
   * of this attribute.
   * Defaults to {@code true}.
   *
   * @return {@code true} if the log should be saved in the database,
   * {@code false} otherwise
   */
  boolean isSaveInDataBase() default true;

}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "system_f_log", schema = "public")
public class LogEntity implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(name = "user_id")
  private String userId;
  @Column(name = "user_login")
  private String userLogin;
  @Column(name = "http_method")
  @Enumerated(EnumType.STRING)
  private HttpMethodEnum httpMethodEnum;
  @Column(name = "url")
  private String url;
  @Column(name = "action")
  @Enumerated(EnumType.STRING)
  private ActionEnum action;
  @Column(name = "action_domain")
  @Enumerated(EnumType.STRING)
  private ActionDomainEnum actionDomain;
  @Column(name = "request_data_change", columnDefinition = "varchar")
  private String requestDataChange;
  @Column(name = "response_data_after_change", columnDefinition = "varchar")
  private String responseDataAfterChange;
  @Column(name = "action_status")
  private String actionStatus;
  @Column(name = "response_status")
  private String responseStatus;
  @Column(name = "dt_create")
  private Timestamp dtCreate;
  @Column(name = "base_exception")
  private String baseException;
  @Column(name = "stack_trace_on_error")
  private String stackTraceOnError;
}
@Component
public class CachingRequestBodyFilter extends GenericFilterBean {

  /**
   * Filters the servlet request and caches the request body for multiple uses.
   *
   * @param servletRequest  the servlet request
   * @param servletResponse the servlet response
   * @param chain           the filter chain
   * @throws IOException      if an I/O error occurs during the filtering process
   * @throws ServletException if a servlet-specific error occurs during the
   * filtering process
   */
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
    throws IOException, ServletException
  {
    HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
    ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);

    chain.doFilter(wrappedRequest, servletResponse);
  }
}
@Component
public class CachingResponseBodyFilter extends GenericFilterBean {

  /**
   * Filters the servlet request and response to cache the response body.
   *
   * @param servletRequest  the servlet request
   * @param servletResponse the servlet response
   * @param chain           the filter chain
   * @throws IOException      if an I/O error occurs during the filtering process
   * @throws ServletException if a servlet-specific error occurs during the
   * filtering process
   */
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
    throws IOException, ServletException
  {
    HttpServletResponse currentResponse = (HttpServletResponse) servletResponse;
    ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(currentResponse);
    try {
      chain.doFilter(servletRequest, wrappedResponse);
    }
    catch (Exception e) {
      e.printStackTrace();
      throw e;
    }
    finally {
      wrappedResponse.copyBodyToResponse();
    }
  }
}
@Repository
public interface LogEntityRepository extends JpaRepository<com.example.pabp_business_logic.aspect.logger.model.LogEntity, Long>, JpaSpecificationExecutor<com.example.pabp_business_logic.aspect.logger.model.LogEntity> {
  /**
   * Saves a {@link com.example.pabp_business_logic.aspect.logger.model.LogEntity}
   * object in the database.
   *
   * <p>It is annotated with {@code @Lock} to specify the lock mode for
   * concurrent access.</p>
   *
   * @param entity the log entity to be saved
   * @return the saved log entity
   */
  @Lock(LockModeType.PESSIMISTIC_WRITE)
  @Override
  com.example.pabp_business_logic.aspect.logger.model.LogEntity save(com.example.pabp_business_logic.aspect.logger.model.LogEntity entity);

  /**
   * Returns a {@link Page} of entities matching the given {@link Specification}.
   *
   * @param spec     can be {@literal null}.
   * @param pageable must not be {@literal null}.
   * @return never {@literal null}.
   */
  @Override
  Page<com.example.pabp_business_logic.aspect.logger.model.LogEntity> findAll(Specification<com.example.pabp_business_logic.aspect.logger.model.LogEntity> spec, Pageable pageable);
}

  @Override
  @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW, timeout = 5)
  @Retryable(retryFor = Exception.class, maxAttempts = 2, backoff = @Backoff(delay = 100))
  public com.example.pabp_business_logic.aspect.logger.model.LogEntity save(com.example.pabp_business_logic.aspect.logger.model.LogEntity entity) {
    try {
      return repository.save(entity);
    }
    catch (Exception e) {
      e.printStackTrace();
      throw e;
    }
  }

  @Override
  @Transactional(timeout = 200, readOnly = true, propagation = Propagation.REQUIRES_NEW)
  public AbstractResponseDTO findAll(SearchRequest request) {
    try {
            ...
    }
    catch (Exception e) {
      e.printStackTrace();
      throw new FilterException(e.getClass().getSimpleName() + " Filter exception: " + e.getMessage());
    }
  }
@Aspect
@Component
public class LoggingAspect {

  @Autowired
  private HttpServletRequest httpRequest;
  @Autowired
  private HttpServletResponse httpResponse;
  @Autowired
  private LogEntityService logEntityService;
  @Autowired
  private UserService userService;
  @Autowired
  private ObjectWriter objectWriter;
  private ContentCachingRequestWrapper requestWrapper;
  private ContentCachingResponseWrapper responseWrapper;
  private LogEntity logEntity;
  private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

  /**
   * Pointcut definition for methods annotated with @ToLogger.
   */
  @Pointcut("@annotation(com.example.pabp_business_logic.aspect.ToLogger)")
  public void callLogger() {
  }

  /**
   * Advice executed before the annotated method execution.
   */
  /*
  In the case of a REST client making a request, the "scheme" field of the
  HttpServletRequest object should not be null. The "scheme" represents the 
  protocol scheme of the request URL, such as "http" or "https". If the "scheme" 
  is null in such cases, it could indicate a problem or misconfiguration in the
  server or the way the request is being handled.
  <p>
  Here are a few possible reasons why the "scheme" field could be null in the
  HttpServletRequest object:
  <p>
  Proxy or Load Balancer: If your application is behind a proxy or load balancer,
  it may affect how the "scheme" field is set in the HttpServletRequest object. 
  In such cases, you may need to ensure that the proxy or load balancer is properly
  configured to forward the scheme information.
  <p>
  Custom Servlet Container or Filter: If you are using a custom servlet container 
  or a filter in your application, it might modify or override the scheme value.
  Make sure your custom components handle the scheme correctly or inspect the 
  configuration to ensure it's not causing the issue.
  <p>
  RequestWrapper or HttpServletRequest Decorator: If you are using a custom 
  RequestWrapper or HttpServletRequest decorator, it's possible that the 
  implementation is not correctly propagating or setting the scheme value. Check
  your custom code to ensure it sets the scheme appropriately.
  <p>
  It's recommended to investigate your application's server configuration, 
  network setup, and any custom components involved in handling the request to 
  determine the specific cause of the "scheme" being null in the 
  HttpServletRequest object.
  */
  @Before("@annotation(com.example.pabp_business_logic.aspect.ToLogger)")
  public void before(JoinPoint joinPoint) {
    try {
      logEntity = new LogEntity();
      initRequest();

      ToLogger toLogger = getLogger(joinPoint);
      if (userService != null) {
        logEntity.setUserId(Optional.ofNullable(userService.getCurrentUserId()).orElse(""));
        if (logEntity.getUserId().equals("anonymous"))
          logEntity.setUserLogin(Optional.of("anonymous").orElse(""));
        else
          logEntity.setUserLogin(Optional.ofNullable(userService.getFio()).orElse(""));
      }
      logEntity.setHttpMethodEnum(toLogger.httpMethod());
      logEntity.setUrl(httpRequest.getRequestURL().toString()); //here's the cause line
      logEntity.setActionDomain(toLogger.actionDomain());
      logEntity.setAction(toLogger.action());

      RequestDataChange requestDataChange = new RequestDataChange();
      List<Params> paramsList = new ArrayList<>();

      Enumeration<String> paramNames = httpRequest.getParameterNames();
      while (paramNames.hasMoreElements()) {
        String name = paramNames.nextElement();
        String[] values = httpRequest.getParameterValues(name);
        paramsList.add(new Params(name, values));
      }
      requestDataChange.setParams(paramsList);
      requestDataChange.setBody(getRequestBody(requestWrapper));

      logEntity.setRequestDataChange(objectWriter.writeValueAsString(requestDataChange).replaceAll(REGEX_FOR_LOGGER, ""));

    }
    catch (Exception e) {
      logger.debug("Error in LoggingAspect.before: " + e.getMessage());
      e.printStackTrace();
    }

  }

  /**
   * Advice executed after the annotated method execution.
   */
  @AfterReturning(value = "@annotation(com.example.pabp_business_logic.aspect.ToLogger)", returning = "returnValue")
  public void afterReturning(JoinPoint joinPoint, Object returnValue) {
    try {
      ToLogger toLogger = getLogger(joinPoint);
      initResponse();
      ResponseDataAfterChange responseDataAfterChange = new ResponseDataAfterChange();

      if (toLogger.returnResponse()) {
        responseDataAfterChange.setBody(getResponseBody(returnValue));
      }

      logEntity.setResponseDataAfterChange(objectWriter.writeValueAsString(responseDataAfterChange).replaceAll(REGEX_FOR_LOGGER, ""));

      logEntity.setResponseStatus(String.valueOf(httpResponse != null ? httpResponse.getStatus() : 0));
      logEntity.setActionStatus(String.valueOf(ActionStatusEnum.SUCCESSFULLY));
      logEntity.setDtCreate(Timestamp.valueOf(LocalDateTime.now()));

      if (toLogger.isSaveInDataBase()) {
        logEntityService.save(logEntity);
      }
      logger.info(logEntity.toString());
    }
    catch (Exception e) {
      logger.debug("Error in LoggingAspect.afterReturning: " + e.getMessage());
      e.printStackTrace();
    }

  }

  /**
   * Advice executed after the annotated method throws an exception.
   */
  @AfterThrowing(value = "@annotation(com.example.pabp_business_logic.aspect.ToLogger)", throwing = "exception")
  public void afterError(JoinPoint joinPoint, Throwable exception) {
    try {
      ToLogger toLogger = getLogger(joinPoint);

      logEntity.setResponseDataAfterChange(null);
      logEntity.setResponseStatus(String.valueOf(500));
      logEntity.setActionStatus(String.valueOf(ActionStatusEnum.ERROR));
      logEntity.setBaseException(exception.getClass().getSimpleName());
      logEntity.setStackTraceOnError(ExceptionUtils.getMessage(exception));
      logEntity.setDtCreate(Timestamp.valueOf(LocalDateTime.now()));

      logEntityService.save(logEntity);
      logger.info(logEntity.toString());

    }
    catch (Exception e) {
      logger.debug("Error in LoggingAspect.afterError: " + e.getMessage());
      e.printStackTrace();
    }
  }

  /**
   * Retrieves the request body from the given request wrapper.
   */
  private String getRequestBody(ContentCachingRequestWrapper request) {
    try {
      String bodyRequest = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
      bodyRequest = bodyRequest.replaceAll(REGEX_FOR_LOGGER, "");
      return bodyRequest;
    }
    catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * Retrieves the response body from the given response object.
   */
  private String getResponseBody(Object response) {
    try {
      if (response == null)
        return null;
      return response.toString();
    }
    catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * Initializes the current request.
   */
  private void initRequest() {
    this.httpRequest = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    this.requestWrapper = (ContentCachingRequestWrapper) httpRequest;
    logger.debug("Init Request httpRequest: " + httpRequest + " " + "requestWrapper: " + requestWrapper);
  }

  /**
   * Initializes the current request.
   */
  private void initResponse() {
    this.httpResponse = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
    this.responseWrapper = (ContentCachingResponseWrapper) httpResponse;
    logger.debug("Init Response httpResponse: " + httpResponse + " " + "responseWrapper: " + responseWrapper);
  }

  /**
   * Retrieves the @ToLogger annotation from the given join point.
   */
  private ToLogger getLogger(JoinPoint joinPoint) {
    /*================Extract information from the annotation===============*/
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    return signature.getMethod().getAnnotation(ToLogger.class);
    /*=====================================================================*/
  }
}

预期工作正确 idk

弹簧 spring-boot spring-mvc nullpointerexception spring-aop

评论

0赞 kriegaex 7/1/2023
欢迎来到 SO。虽然尝试在这里发布一个最小的复制者是值得称赞的,但你的代码并不代表一个:(1)没有人愿意复制和粘贴6个类并猜测几十个提交,其中一些在我的IDE中有多个建议。(2)尽管如此,仍然缺少几个类,示例类中的包名称,Spring配置和一些驱动程序应用程序,即没有人可以重现该问题。因此,请将一个完整的、最小的复制器推送到 GitHub。那我当然可以看一看。只是在我脑海中解析代码是无济于事的。
0赞 kriegaex 7/1/2023
顺便说一句,您是否阅读了方面代码中有关方案的评论?是你写的还是别人写的?如果是其他人,你能问问作者吗?类似或相应的 IDE 功能可以显示谁提交了该注释。git annotate
0赞 ogbozoyan 7/1/2023
那个记录器是我为我们的系统发明记录器的工作任务,我留下了我对错误来源的怀疑的评论 这是 github.com/ogbozoyan/stack-overflow-logger 感谢您的回复,我已经为这个问题花了 2 周的时间
0赞 kriegaex 7/2/2023
您能否更新项目,以便我可以在不设置 Docker 或数据库服务器的情况下实际运行应用程序?我收到错误:APPLICATION FAILED TO START ... Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class. Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
0赞 ogbozoyan 7/3/2023
更新,添加了 H2 和 Swagger 以简化操作

答:

0赞 ogbozoyan 7/3/2023 #1

我解决了这个问题。因此,在 UserService 中是调用其他服务和 idk 的方法,它设置了 null HttpServletResponse 对象:D删除了现在效果更好的内容

评论

0赞 kriegaex 7/3/2023
这个答案没有帮助。这也是不正确的,请参阅我之前的评论。请给自己的帖子更多的爱,并考虑如何让它们对其他社区成员有所帮助。这是 SO 上的给予和接受。你到底改变了什么,它是如何解决问题的。代码在哪里?谢谢。
0赞 ogbozoyan 7/4/2023
我无法共享UserService代码部分,但我可以说出那里有什么:logEntity.setUserLogin(Optional.ofNullable(userService.getFio()).orElse(“”));在 userservice.getFio() 中是对其他服务的改造请求,我正在池化,而没有从该服务中得到答案,我只是重构了该方法,将逻辑从另一个服务移动到我的,仅此而已 也许问题出在池化谁知道
0赞 kriegaex 7/4/2023
请学习如何提问和回答问题。你不能共享代码 - 恕我直言 - 是一个蹩脚的借口。没有人需要你分类的原始代码,只要足以理解问题及其解决方案。随意匿名代码,重命名内容,等等。还请将代码从您的评论移动到您的答案中(有一个编辑按钮),正确格式化并解释更多。这个答案太草率了。很抱歉这么直白,我的意思是指导你在这里写出更好的答案。这样的答案只会招致反对票。
0赞 Community 7/7/2023
正如目前所写的那样,你的答案尚不清楚。请编辑以添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。您可以在帮助中心找到有关如何写出好答案的更多信息。