2016년 2월 15일 월요일

Name for argument type [java.lang.String] not available 해결하기

Spring의 Controller를 사용할 때 아래와 같은 IllegalArgumentException이 발생하여 해결한 내용을 남긴다.


1.
어느날 부터 로컬 tomcat 서버에서는 에러가 안나는데 개발서버에서는 아래와 같은 에러가 발생하기 시작했다.

java.lang.IllegalArgumentException: Name for argument type [java.lang.String] not available, and parameter name information not found in class file either.
        at org.springframework.util.Assert.notNull(Assert.java:112) ~[spring-core-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.updateNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:141) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.getNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:120) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:87) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:79) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
... 이하 생략

2.
"Name for argument type [java.lang.String] not available"으로 검색해보니 3번째로 검색되는 블로그에 문제의 원인과 해결책이 제시되어 있었다. Controller 메서드의 파라메터를 HttpRequest의 파라메터로 연결해서 사용하려면 @ReqParam 어노테이션을 주고 value도 꼭 주어야 한다는 내용이다. 그래야 javac가 -g:none 옵션으로 컴파일이 되어도 파라메터 매핑이 에러없이 되다는 것이다.

이런 코드가
public String viewCliRrpt(HttpServletRequest request, String prjtId, String samplId) {

이렇게 바뀌어야 한다.
public String viewCliRrpt(HttpServletRequest request
, @RequestParam(value = "prjtId") String prjtId
, @RequestParam(value = "samplId") String samplId) {

3.
어느날 갑자기 이런 에러가 발생한 이유는, 바로 그 어느날에 빌드스크립트를 정리하면서 ant의 javac 태스크의 debug="true" 옵션을 제거했기 때문이었다. 그날 부터 서버에 배포되는 war는 -g:none 옵션으로 컴파일된 class파일들이 포함되었고, RequestParam 어노테이션이 없었기 때문에 Exception이 발생했었다.

4.
그런데 개발서버에서만 에러가 발생하고 로컬 서버에서는 정상이었던 이유는 이클립스가 기본으로 debug 옵션을 포함하여 자바를 컴파일하기 때문이다. 여기에서 그 옵션이 무엇인지를 찾았고, 해당 옵션의 설명은 이클립스 헬프페이지에서 확인이 가능하다. 하지만 -g:none 옵션과 연결되어 설명되어 있지는 않다.

5.
결론은 빌드스크립트에서 debug="true" 옵션을 추가하는 것으로 마무리하였다. 개발서버에서도 debug log를 보는 경우가 허다하고, 실제 서비스되는 war라 하더라도 유지보수차원에서 로그를 확인해야 하는 경우가 생길 것이라고 판단했기 때문이다.


2016년 2월 12일 금요일

Spring의 SimpleMappingExceptionResolver에서 로그 남기기

회사에서 개발중인 시스템에서 Spring의 SimpleMappingExceptionResolver와 관련된 부분을 수정했기에 블로그로 남긴다.

1.
Web프레임웍으로 Anyframe을 사용중인데, *.do 요청을 AJAX로 보내면 Exception 발생시에도 stacktrace를 로그에 남기지 않아서 불편한 점이 있었다. 그동안은 *.do 요청이 많지 않아서 불편함이 드러나지 않았는데, 최근에 *.do 요청으로 중요 로직을 연결하여 사용하다보니 불편함이 느껴져서 고치게 되었다.

2.
Exception유형에 따른 View를 결정할때 Anyframe의 AjaxServiceExceptionHandler를 사용하고 있다. AjaxServiceExceptionHandler는 Spring의 SimpleMappingExceptionResolver를 상속받은 클래스이고, HttpRequestHeader에 X-Requested-With 헤더값이 XMLHttpRequest일때는 super.resolveException를 호출하지 않도록 구현되어 있다.

3.
좀 더 찾아보니 SimpleMappingExceptionResolver는 AbstractHandlerExceptionResolver를 상속받았기 때문에 warnLogCategory를 사용하는 방법이 있었다. WarnLogCategory에 logger의 이름을 넣어주면 해당 logger로 warn 레벨의 로그를 남겨주게 되어 있다.

spring-webmvc-4.0.3.RELEASE.jar:AbstractHandlerExceptionResolver.class의 일부

            public void setWarnLogCategory(String loggerName)
            {
/* 108*/        warnLogger = LogFactory.getLog(loggerName);
            }

            protected void logException(Exception ex, HttpServletRequest request)
            {
/* 186*/        if(warnLogger != null && warnLogger.isWarnEnabled())
/* 187*/            warnLogger.warn(buildLogMessage(ex, request), ex);
            }

그런데 2번에서 언급한 것 처럼 super를 호출하지 않기 때문에 빈생성시 warnLogCategory 프러퍼티를 넣어주어도 로그가 남지 않고 있었다.

4.
AjaxServiceExceptionHandler가 super.resolveException를 항상 호출하도록 수정할 수 없으니, AbstractHandlerExceptionResolver의 warnLogCategory를 활용하는 방법으로 해결하는 것은 포기하고, 검색하다 발견한 블로그처럼 AjaxServiceExceptionHandler를 상속받은 LogAjaxServiceExceptionHandler를 새로 만들어서 사용하기로 했다.

public class LogAjaxServiceExceptionHandler extends AjaxServiceExceptionHandler {

private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
logger.error(ex.getMessage(), ex);
return super.resolveException(request, response, handler, ex);
}

}

5.
logger를 새로 생성하지 않고 AbstractHandlerExceptionResolver가 가지고 있는 logger를 사용해도 무방하다. 다만, 나중에 logger의 이름을 변경하게 될 것 같아서 별도로 생성해 놨다.

6.
검색하다 보니 stacktrace를 로그로 남기는 방법을 직접 구현한 블로그도 있었는데, logger에 2번째 파라미터로 Exception을 넘겨주면 알아서 stacktrace도 찍어주기 때문에 불필요했다.