기존에는 @ExceptionHandler
을 붙인 메소드에서 해당하는 Exception만 메소드의 인자로 전달받아 사용했으나,
보상 트랜젝션을 수행을 검토하면서, Exception 외의 받을 수 있는 argument는 어떤게 있고, argument에 값이 전달되는 원리가 궁금해 찾아보게되었다.
@ExceptionHandler
메소드에 올 수 있는 Parameter대표적으론 다음과 같다.
그래서 아래와 ExceptionHandler method를 선언하면 실행 시, 알아서 값이 들어온다.
@ExceptionHandler(Exception.class)
public CommonResponse handleException(Exception e, HttpServletRequest request,
HttpServletResponse response, HttpMethod method) {
log.error(e.getMessage(), e);
return createErrorResponse(ERR_UNEXPECTED);
}
만약 지원하지 않는 타입이 들어오면 아래와 같은 에러가 발생한다.
2025-02-16 02:01:54.806 WARN 55113 --- [nio-8081-exec-1] .m.m.a.ExceptionHandlerExceptionResolver : Failure in @ExceptionHandler com.controller.ControllerAdvice#handleException(Exception, HttpServletRequest, HttpServletResponse, RedirectAttributes, HttpSession, Model, HttpMethod, Principal, String)
java.lang.IllegalStateException: Could not resolve parameter [8] in public com.CommonResponse<java.lang.Object> com.controller.ControllerAdvice.handleException(java.lang.Exception,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,org.springframework.web.servlet.mvc.support.RedirectAttributes,javax.servlet.http.HttpSession,org.springframework.ui.Model,org.springframework.http.HttpMethod,java.security.Principal,java.lang.String):
No suitable resolver at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:176)
ExceptionHandlerExceptionResolver.getDefaultArgumentResolvers() 반환 값의 Resolver들이 지원하는 타입이 들어올 수 있다고 보면 된다 (아래 코드 참조. Custom도 가능)
ExceptionHandlerExceptionResolver
가 생성될 때, @ControllerAdvice
가 붙은 빈을 검색하고, 각 빈에서 @ExceptionHandler
가 붙은 메서드를 찾아 저장한다. getDefaultArgumentResolvers()
에서 Method에 argument를 처리해주는 HandlerMethodArgumentResolver
들을 지정한다. DispatcherServlet
이 적절한 @ExceptionHandler
메서드를 탐색한다. ExceptionHandlerMethodResolver
가 해당 예외를 처리할 수 있는 메서드를 찾아 ServletInvocableHandlerMethod
로 래핑한다. ServletInvocableHandlerMethod
는 InvocableHandlerMethod.invokeForRequest()
를 호출하여 HandlerMethodArgumentResolverComposite
을 통해 메서드 인자로 넣을 값들을 찾은 후, method.invoke(bean, args)
를 실행하여 @ExceptionHandler
메서드를 호출한다.DispatcherServlet에서 부터 @ExceptionHandler가 붙은 method를 실행하기까지 필요한 코드들만 간추렸다.
주석의 번호가 적힌 함수들을 위주로 따라가면 된다.
public class DispatcherServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// ...
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
// 1. handler 호출
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
// 2. 에러 저장
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 3. result 수행
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
}
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
// exception 처리
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
if (this.handlerExceptionResolvers != null) {
// HandlerExceptionResolver를 구현하는 빈들을 초기화 시 저장 (ExceptionHandlerExceptionResolver도 그 중 하나)
for(HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
}
}
}
}
public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
@Nullable
protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod)handler : null;
return this.doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
@Nullable
protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
}
// ControllerAdvice들을 찾아 가지고 있음
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 4. getExceptionHandlerMethod는 exception과 매칭되는 ExceptionHandler가 붙은 Method를 찾아 ServletInvocableHandlerMethod에 넣음
ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception);
if (this.argumentResolvers != null) {
// 0-1의 resolver들을 넣은 composite 객체를 넣음
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
ArrayList<Throwable> exceptions = new ArrayList();
// 5. exception 객체들은 인자의 앞에 순서대로 넣음
Object[] arguments = new Object[exceptions.size() + 1];
exceptions.toArray(arguments);
arguments[arguments.length - 1] = handlerMethod;
// 6. exception을 처리하는 메소드를 실행
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
}
@Override
public void afterPropertiesSet() {
// 0. ControllerAdvice를 전부 scan
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
// 0-1. Method의 Argument들을 넣을 수 있도록 해주는 Resolver들을 초기에 지정
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
}
protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
// Annotation-based argument resolution
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
return resolvers;
}
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
// 해당 exception을 처리하는 method를 전달
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
//ExceptionHandlerExceptionResolver 생성 시 설정
private void initExceptionHandlerAdviceCache() {
// ControllerAdvice bean을 찾아 adviceBeans 반환
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
// @ExceptionHandler 를 가진 method들을 가지고 있음
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
}
}
}
public class ControllerAdviceBean implements Ordered {
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {
if (!ScopedProxyUtils.isScopedTarget(name)) {
ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class);
if (controllerAdvice != null) {
adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));
}
}
}
OrderComparator.sort(adviceBeans);
return adviceBeans;
}
}
public class ExceptionHandlerMethodResolver {
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
}
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
}
}
public class InvocableHandlerMethod extends HandlerMethod {
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
public void setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
this.resolvers = argumentResolvers;
}
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 7. 파라미터로 넣을 값들을 찾음
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
// 8. 인자값으로 메소드 호출 및 return 값 전달
return doInvoke(args);
}
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
try {
// 7-1. 파라미터 별로 맞는 인자를 찾기위해 호출
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
}
return args;
}
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
return method.invoke(getBean(), args);
}
}
}
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 7-2. parameter에 맞는 resolver를 찾음 (0-1. 의 List 중에서 해당 paramter를 지원하는 resolver)
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
// 7-3. paramter에 맞는 인자를 리턴
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
}
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
*/
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ExceptionHandlerExceptionResolver.. ExceptionHandlerMethodResolver... 이름이 다들 비슷해서 코드 따라가는데 시간이 한참 걸렸지만 재미있었다.