2011-03-03 16 views
9

Estoy utilizando la anotación de Spring @ExceptionHandler para detectar excepciones en mis controladores.Leyendo el contenido de httprequest del controlador de excepción de primavera

Algunas solicitudes contienen datos POST como cadena XML simple escrita en el cuerpo de la solicitud, quiero leer esos datos para registrar la excepción. El problema es que cuando solicito el inputstream en el manejador de excepciones y trato de leer de él, el flujo devuelve -1 (vacío).

La firma de gestión de excepciones es:

@ExceptionHandler(Throwable.class) 
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) 

¿Alguna idea? ¿Hay alguna forma de acceder al cuerpo de la solicitud?

Mi controlador:

@Controller 
@RequestMapping("/user/**") 
public class UserController { 

    static final Logger LOG = LoggerFactory.getLogger(UserController.class); 

    @Autowired 
    IUserService userService; 


    @RequestMapping("/user") 
    public ModelAndView getCurrent() { 
     return new ModelAndView("user","response", userService.getCurrent()); 
    } 

    @RequestMapping("/user/firstLogin") 
    public ModelAndView firstLogin(HttpSession session) { 
     userService.logUser(session.getId()); 
     userService.setOriginalAuthority(); 
     return new ModelAndView("user","response", userService.getCurrent()); 
    } 


    @RequestMapping("/user/login/failure") 
    public ModelAndView loginFailed() { 
     LOG.debug("loginFailed()"); 
     Status status = new Status(-1,"Bad login"); 
     return new ModelAndView("/user/login/failure", "response",status); 
    } 

    @RequestMapping("/user/login/unauthorized") 
    public ModelAndView unauthorized() { 
     LOG.debug("unauthorized()"); 
     Status status = new Status(-1,"Unauthorized.Please login first."); 
     return new ModelAndView("/user/login/unauthorized","response",status); 
    } 

    @RequestMapping("/user/logout/success") 
    public ModelAndView logoutSuccess() { 
     LOG.debug("logout()"); 
     Status status = new Status(0,"Successful logout"); 
     return new ModelAndView("/user/logout/success", "response",status); 

    } 

    @RequestMapping(value = "/user/{id}", method = RequestMethod.POST) 
    public ModelAndView create(@RequestBody UserDTO userDTO, @PathVariable("id") Long id) { 
     return new ModelAndView("user", "response", userService.create(userDTO, id)); 
    } 

    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) 
    public ModelAndView getUserById(@PathVariable("id") Long id) { 
     return new ModelAndView("user", "response", userService.getUserById(id)); 
    } 

    @RequestMapping(value = "/user/update/{id}", method = RequestMethod.POST) 
    public ModelAndView update(@RequestBody UserDTO userDTO, @PathVariable("id") Long id) { 
     return new ModelAndView("user", "response", userService.update(userDTO, id)); 
    } 

    @RequestMapping(value = "/user/all", method = RequestMethod.GET) 
    public ModelAndView list() { 
     return new ModelAndView("user", "response", userService.list()); 
    } 

    @RequestMapping(value = "/user/allowedAccounts", method = RequestMethod.GET) 
    public ModelAndView getAllowedAccounts() { 
     return new ModelAndView("user", "response", userService.getAllowedAccounts()); 
    } 

    @RequestMapping(value = "/user/changeAccount/{accountId}", method = RequestMethod.GET) 
    public ModelAndView changeAccount(@PathVariable("accountId") Long accountId) { 
     Status st = userService.changeAccount(accountId); 
     if (st.code != -1) { 
      return getCurrent(); 
     } 
     else { 
      return new ModelAndView("user", "response", st); 
     } 
    } 
    /* 
    @RequestMapping(value = "/user/logout", method = RequestMethod.GET) 
    public void perLogout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 
     userService.setOriginalAuthority(); 
     response.sendRedirect("/marketplace/user/logout/spring"); 
    } 
    */ 

    @ExceptionHandler(Throwable.class) 
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) { 
    Status st = new Status(); 
    try { 
     Writer writer = new StringWriter(); 
     byte[] buffer = new byte[1024]; 

     //Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream())); 
     InputStream reader = request.getInputStream(); 
     int n; 
     while ((n = reader.read(buffer)) != -1) { 
      writer.toString(); 

     } 
     String retval = writer.toString(); 
     retval = ""; 
     } catch (IOException e) { 

      e.printStackTrace(); 
     } 

     return new ModelAndView("profile", "response", st); 
    } 
} 

Gracias

Respuesta

9

que he probado su código y he encontrado algunos errores en la gestión de excepciones, cuando se lee desde el InputStream:

Writer writer = new StringWriter(); 
byte[] buffer = new byte[1024]; 

//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream())); 
InputStream reader = request.getInputStream(); 
int n; 
while ((n = reader.read(buffer)) != -1) { 
    writer.toString(); 

} 
String retval = writer.toString(); 
retval = ""; 

He reemplazado su código con este:

BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream())); 
String line = ""; 
StringBuilder stringBuilder = new StringBuilder(); 
while ((line=reader.readLine()) != null) { 
    stringBuilder.append(line).append("\n"); 
} 

String retval = stringBuilder.toString(); 

Luego, puedo leer desde InputStream en el manejador de excepciones, ¡funciona! Si aún no puede leer desde InputStream, le sugiero que compruebe cómo PUBLICA datos xml al cuerpo de la solicitud. Debería considerar que puede consumir el Inputstream solo una vez por solicitud, por lo que le sugiero que compruebe que no haya ninguna otra llamada al getInputStream(). Si tiene que llamarlo dos o más veces, debe escribir un HttpServletRequestWrapper personalizado de esta manera para hacer una copia del cuerpo de la solicitud, para que pueda leerlo más veces.

ACTUALIZACIÓN
Sus comentarios me ha ayudado a reproducir el problema. Utiliza la anotación @RequestBody, por lo que es cierto que no llama al getInputStream(), pero Spring lo invoca para recuperar el cuerpo de la solicitud. Eche un vistazo a la clase org.springframework.web.bind.annotation.support.HandlerMethodInvoker: si usa @RequestBody esta clase invoca el método resolveRequestBody, y así sucesivamente ... finalmente ya no puede leer el InputStream de su ServletRequest. Si aún desea utilizar ambos @RequestBody y getInputStream() en su propio método, debe envolver la solicitud en un HttpServletRequestWrapper personalizado para hacer una copia del cuerpo de la solicitud, para que pueda leerla manualmente más veces. Este es mi envoltorio:

public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper { 

    private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class); 
    private final String body; 

    public CustomHttpServletRequestWrapper(HttpServletRequest request) { 
     super(request); 

     StringBuilder stringBuilder = new StringBuilder(); 
     BufferedReader bufferedReader = null; 

     try { 
      InputStream inputStream = request.getInputStream(); 
      if (inputStream != null) { 
       bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); 
       String line = ""; 
       while ((line = bufferedReader.readLine()) != null) { 
        stringBuilder.append(line).append("\n"); 
       } 
      } else { 
       stringBuilder.append(""); 
      } 
     } catch (IOException ex) { 
      logger.error("Error reading the request body..."); 
     } finally { 
      if (bufferedReader != null) { 
       try { 
        bufferedReader.close(); 
       } catch (IOException ex) { 
        logger.error("Error closing bufferedReader..."); 
       } 
      } 
     } 

     body = stringBuilder.toString(); 
    } 

    @Override 
    public ServletInputStream getInputStream() throws IOException { 
     final StringReader reader = new StringReader(body); 
     ServletInputStream inputStream = new ServletInputStream() { 
      public int read() throws IOException { 
       return reader.read(); 
      } 
     }; 
     return inputStream; 
    } 
} 

entonces debería escribir un simple Filter para envolver la solicitud:

public class MyFilter implements Filter { 

    public void init(FilterConfig fc) throws ServletException { 

    } 

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
     chain.doFilter(new CustomHttpServletRequestWrapper((HttpServletRequest)request), response); 

    } 

    public void destroy() { 

    } 

} 

Por último, hay que configurar el filtro en su web.xml:

<filter>  
    <filter-name>MyFilter</filter-name> 
    <filter-class>test.MyFilter</filter-class> 
</filter> 
<filter-mapping> 
    <filter-name>MyFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

Puede disparar su filtro solo para los controladores que realmente lo necesitan, por lo que debe cambiar el patrón de url según sus necesidades.

Si necesita esta característica en un solo controlador, también puede hacer una copia del cuerpo de la solicitud en ese controlador cuando lo reciba a través de la anotación @RequestBody.

+0

Gracias por su respuesta, todavía no consigo leer el cuerpo de la solicitud. No se lanza ninguna excepción, es solo que el lector devuelve nulo, lo que significa que está vacío. Tal vez me esté perdiendo algo con respecto a los manejadores de excepciones en la primavera. ¿Cómo puede leer la solicitud? –

+0

Acabo de copiar su código (con mis correcciones) a un nuevo proyecto. Llamo al controlador a través de un formulario html estático con enctype = "multipart/form-data" y un input type = "file" para cargar un archivo. He agregado una anotación RequestMapping y el método correspondiente que solo arroja una RuntimeException. En el método del controlador de excepciones, puedo registrar todo el archivo cargado a través del formulario. Parece que su flujo de entrada está vacío cuando ingresa al controlador o ya se consume a través de otra llamada a getInputStream(). ¿Qué versión de Spring? – javanna

+0

@Noam Nevo ¿Alguna noticia? :-) – javanna

5

Tuve el mismo problema y lo resolví con HttpServletRequestWrapper como se describió anteriormente y funcionó muy bien. Pero luego, encontré otra solución para extender HttpMessageConverter, en mi caso era MappingJackson2HttpMessageConverter.

public class CustomJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter{ 

    public static final String REQUEST_BODY_ATTRIBUTE_NAME = "key.to.requestBody"; 


    @Override 
    public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { 

     final ByteArrayOutputStream writerStream = new ByteArrayOutputStream(); 

     HttpInputMessage message = new HttpInputMessage() { 
      @Override 
      public HttpHeaders getHeaders() { 
       return inputMessage.getHeaders(); 
      } 
      @Override 
      public InputStream getBody() throws IOException { 
       return new TeeInputStream(inputMessage.getBody(), writerStream); 
      } 
     }; 
        RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_BODY_ATTRIBUTE_NAME, writerStream, RequestAttributes.SCOPE_REQUEST); 

     return super.read(type, contextClass, message); 
    } 

} 

com.sun.xml.internal.messaging.saaj.util.TeeInputStream se utiliza.

En la primavera de configuración MVC

<mvc:annotation-driven > 
    <mvc:message-converters> 
     <bean class="com.company.remote.rest.util.CustomJsonHttpMessageConverter" /> 
    </mvc:message-converters> 
</mvc:annotation-driven> 

En el método @ExceptionHandler

@ExceptionHandler(Exception.class) 
public ResponseEntity<RestError> handleException(Exception e, HttpServletRequest httpRequest) { 

    RestError error = new RestError(); 
    error.setErrorCode(ErrorCodes.UNKNOWN_ERROR.getErrorCode()); 
    error.setDescription(ErrorCodes.UNKNOWN_ERROR.getDescription()); 
    error.setDescription(e.getMessage()); 


    logRestException(httpRequest, e); 

    ResponseEntity<RestError> responseEntity = new ResponseEntity<RestError>(error,HttpStatus.INTERNAL_SERVER_ERROR); 
    return responseEntity; 
} 

private void logRestException(HttpServletRequest request, Exception ex) { 
    StringWriter sb = new StringWriter(); 
    sb.append("Rest Error \n"); 
    sb.append("\nRequest Path"); 
    sb.append("\n----------------------------------------------------------------\n"); 
    sb.append(request.getRequestURL()); 
    sb.append("\n----------------------------------------------------------------\n"); 
Object requestBody = request.getAttribute(CustomJsonHttpMessageConverter.REQUEST_BODY_ATTRIBUTE_NAME); 

    if(requestBody != null) { 
     sb.append("\nRequest Body\n"); 
     sb.append("----------------------------------------------------------------\n"); 
     sb.append(requestBody.toString()); 

     sb.append("\n----------------------------------------------------------------\n"); 
    } 

    LOG.error(sb.toString()); 
} 

espero que ayude :)

+0

lo hizo:) ...... – masadwin

+0

TeeInputStream acaba de funcionar para XML? – Frank

2

Recientemente he enfrentado a este problema y lo resolvió de forma ligeramente diferente. Con bota de resorte 1.3.5.RELEASE

El filtro se implementó con la clase Spring ContentCachingRequestWrapper. Este contenedor tiene un método getContentAsByteArray() que se puede invocar varias veces.

import org.springframework.web.util.ContentCachingRequestWrapper; 
public class RequestBodyCachingFilter implements Filter { 

    public void init(FilterConfig fc) throws ServletException { 
    } 

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
     chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest)request), response); 
    } 

    public void destroy() { 
    } 
} 

añadido el filtro a la cadena

@Bean 
public RequestBodyCachingFilter requestBodyCachingFilter() { 
    log.debug("Registering Request Body Caching filter"); 
    return new RequestBodyCachingFilter(); 
} 

En el controlador de excepciones.

@ControllerAdvice(annotations = RestController.class) 
public class GlobalExceptionHandlingControllerAdvice { 
    private ContentCachingRequestWrapper getUnderlyingCachingRequest(ServletRequest request) { 
     if (ContentCachingRequestWrapper.class.isAssignableFrom(request.getClass())) { 
      return (ContentCachingRequestWrapper) request; 
     } 
     if (request instanceof ServletRequestWrapper) { 
      return getUnderlyingCachingRequest(((ServletRequestWrapper)request).getRequest()); 
     } 
     return null; 
    } 

    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 
    @ExceptionHandler(Throwable.class) 
    public @ResponseBody Map<String, String> conflict(Throwable exception, HttpServletRequest request) { 
     ContentCachingRequestWrapper underlyingCachingRequest = getUnderlyingCachingRequest(request); 
     String body = new String(underlyingCachingRequest.getContentAsByteArray(),Charsets.UTF_8); 
     .... 
    } 
} 
+0

hasta ahora la mejor/limpia respuesta, ¡gracias! –

Cuestiones relacionadas