【JavaEE进阶】Spring统一功能处理:统一异常处理和统一结果返回

目录

1.统一数据返回格式

1.1快速入门

1.2存在问题

1.3 案例代码修改

 1.4 优点

2.统一异常处理

3.源码分析

3.1 @ControllerAdvice 源码分析

3.2 1.2中返回String会报错原因分析


1.统一数据返回格式

在图书管理系统中的强制登录的过程中, 我们共做了两步工作

1.通过 Session 来判断用户是否登录

2.对后端返回数据进行封装, 告知前端处理的结果

回顾

后端统一返回的结果格式如下:

@Data
public class Result<T> {
    private ResultStatus  code; //业务码 200 - 成功 -2 失败 -1 未登录
    private String errMsg; //错误信息 业务成功, errMsg为空
    private T data;
}

后端逻辑处理:

@RequestMapping("/getBookListByPage")
    public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest, HttpSession session) {
        log.info("查询图书列表, 请求参数pageRequest:{}", pageRequest);
        //用户登录, 返回图书列表
        PageResult<BookInfo> bookList = bookService.getBookListByPage(pageRequest);
        return Result.success(bookList);
    }

Result.success(pageResult) 就是对返回数据进行了封装.

拦截器帮我们实现了第一个功能, 接下来看 SpringBoot 对第二个功能如何支持.

1.1快速入门

统一数据返回格式使用 @ControllerAdviceResponseBodyAdvice 的方式来实现.

@ControllerAdvice 表示控制器通知类

添加 ResponseAdvice, 实现 ResonseBodyAdvice 接口, 并在类上添加 @ControllerAdvice注解

import com.example.com.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}

supports方法: 判断是否要执行 beforeBodyWrite 方法.  true为执行,  false不执行.  通过该方法可以
选择哪些类或哪些方法的 response 要进行处理,其他的不进行处理.
 

returnType 获取类名和方法名:

//获取执⾏的类 
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执⾏的⽅法 
Method method = returnType.getMethod();

beforeBodyWrite 方法:  对 response 方法进行具体操作处理

测试: 

添加统一数据返回格式之前:

添加统一数据返回格式之后:

1.2存在问题

问题现象:

我们继续测试修改图书的接口:

结果显示, 发生内部错误

查看数据库, 发现数据操作成功

日志报错信息如下:

在多次测试不同的返回结果之后, 发现只有返回结果为 String 类型时才有这种情况发生

测试代码:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "string";
    }

    @RequestMapping("/t2")
    public Integer t2() {
        return 1;
    }

    @RequestMapping("/t3")
    public Boolean t3() {
        return true;
    }
}

解决方案:

import com.example.com.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果返回结果为String类型, 使用SpringBoot内置提供的Jackson来实现信息的序列化
        if(body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

重新测试, 结果返回正常:

1.3 案例代码修改

如果一些方法返回的结果已经时 Result 类型了, 那就直接返回 Result 类型的结果即可

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //返回结果更加灵活
        if(body instanceof Result) {
            return body;
        }
        //如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
        if(body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

 1.4 优点

1.方便前端程序员更好的接收和解析后端数据接口返回的数据

2.降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,  因为所有接口都是这样返回的.

3.有利于项目统一数据的维护和修改.

4.有利于后端技术部门门]的统一规范的标准制定,不会出现稀奇古怪的返回内容.
 

2.统一异常处理

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice表示控制器通知类,  @ExceptionHandler 是异常处理器, 两个结合表示具体代码如下:

import com.example.com.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
    @ExceptionHandler
    public Result handlerException(Exception e) {
        log.error("发生异常,e:", e);
        return Result.fail("内部错误");
    }
}

类名, 方法名和返回值可以自定义, 重要的是注解

接口返回为数据是, 需要加上 @ResponseBody 注解

以上代码表示, 如果代码出现 Exception 异常(包括Exception的子类), 就返回一个 Result 的对象, Result 对象的设置参考 Result.fail(e.getMessage())

public static Result fail(String msg) {
        Result result = new Result<>();
        result.setCode(ResultStatus.FAIL);
        result.setErrMsg(msg);
        return result;
    }

我们可以针对不同的异常, 返回不同的结果:

import com.example.com.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
    @ExceptionHandler
    public Result handlerException(Exception e) {
        log.error("发生异常,e:", e);
        return Result.fail(e.getMessage());
    }

    @ExceptionHandler
    public Result handlerException(NullPointerException e) {
        log.error("发生异常,e:", e);
        return Result.fail("发生空指针异常: " + e.getMessage());
    }

    @ExceptionHandler
    public Result handlerException(ArithmeticException e) {
        log.error("发生异常,e:", e);
        return Result.fail("发生算数异常: " + e.getMessage());
    }
}

模拟制造异常:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        int a = 10/0; //抛出ArithmeticException
        return "string";
    }

    @RequestMapping("/t2")
    public Integer t2() {
        String a =null;
        System.out.println(a.length()); //抛出NullPointerException
        return 1;
    }
}

当有多个异常通知时, 匹配顺序为当前类及其子类向上一次匹配

/test/t1 抛出ArithmeticException, 运行结果如下:

/test/t2 抛出NullPointerException, 运行结果如下:

3.源码分析

3.1 @ControllerAdvice 源码分析

统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的, 通过分析 @ControllerAdvice的源码, 可以知道它们的执行流程:

点击 @ControllerAdvice 实现源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor(
        annotation = Component.class,
        attribute = "value"
    )
    String name() default "";

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

从上述源码可以看出, @ControllerAdvice 派生与@Component 组件, 这也就是为什么没有五大注解, ControllerAdvice 就生效的原因.

下面我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.

DispatcherServlet 对象在创建时会初始化一系列的对象:

public class DispatcherServlet extends FrameworkServlet {
    //...
    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

    /**
     * Initialize the strategy objects that this servlet uses.
     * <p>May be overridden in subclasses in order to initialize further
     * strategy objects.
     */
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    //...
}

对于 @ControllerAdvice 注解, 我们重点关注 initHandlerAdapters(context) 和 initHandlerExceptionResolvers(context) 这两个方法. 

1.initHandlerAdapters(context)

initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的 bean并保存起来, 其中有一个类型为 RequestMappingHandlerAdapterbean , 这个 bean 就是 @RequestMapping 注解能起作用的关键, 这个在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象, 并做进一步处理, 关键代码如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
    //...


    /**
     * 添加ControllerAdvice bean的处理
     */
    private void initControllerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }

        //获取所有所有被 @ControllerAdvice 注解标注的bean对象 
        List<ControllerAdviceBean> adviceBeans =
                ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());

        List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for 
                        ControllerAdviceBean:" + adviceBean);
            }
            Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType,
                    MODEL_ATTRIBUTE_METHODS);
            if (!attrMethods.isEmpty()) {
                this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
            }
            Set<Method> binderMethods =
                    MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
            if (!binderMethods.isEmpty()) {
                this.initBinderAdviceCache.put(adviceBean, binderMethods);
            }
            if (RequestBodyAdvice.class.isAssignableFrom(beanType) ||
                    ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                requestResponseBodyAdviceBeans.add(adviceBean);
            }
        }

        if (!requestResponseBodyAdviceBeans.isEmpty()) {
            this.requestResponseBodyAdvice.addAll(0,
                    requestResponseBodyAdviceBeans);
        }

        if (logger.isDebugEnabled()) {
            int modelSize = this.modelAttributeAdviceCache.size();
            int binderSize = this.initBinderAdviceCache.size();
            int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
            int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
            if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount
                    == 0) {
                logger.debug("ControllerAdvice beans: none");
            } else {
                logger.debug("ControllerAdvice beans: " + modelSize + " 
                @ModelAttribute," + binderSize +
                " @InitBinder, " + reqCount + " RequestBodyAdvice, " +
                        resCount + " ResponseBodyAdvice");
            }
        }
    }
    //...


}

这个方法在执行时会查找使用所有的 @ControllerAdvice 类, 把ResponseBodyAdvice 类放在容器中, 当发生某个事件时, 调用相应的 Advice 方法, 比如返回数据前调用统一数据封装

至于 DispatcherServletRequestMappingHandlerAdapter 时如何交互的这就是另一个复杂的话题了, 此处就不再赘述, 以了解为主. 

2.initHandlerExceptionResolvers(context)

接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) 方法, 这个方法会取得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来, 其中就有一个类型为 ExceptionHandlerExceptionResolver bean, 这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步处理, 代码如下:

public class ExceptionHandlerExceptionResolver
        extends AbstractHandlerMethodExceptionResolver
        implements ApplicationContextAware, InitializingBean {

    //...

    private void initExceptionHandlerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }

        // 获取所有所有被 @ControllerAdvice 注解标注的bean对象
        List<ControllerAdviceBean> adviceBeans = 
                ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for  ControllerAdviceBean:" + adviceBean);
            } ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            if (resolver.hasExceptionMappings()) {
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
            }
            if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                this.responseBodyAdvice.add(adviceBean);
            }
        }

        if (logger.isDebugEnabled()) {
            int handlerSize = this.exceptionHandlerAdviceCache.size();
            int adviceSize = this.responseBodyAdvice.size();
            if (handlerSize == 0 && adviceSize == 0) {
                logger.debug("ControllerAdvice beans: none");
            } else {
                logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + 
                        adviceSize + "  ResponseBodyAdvice");
            }
        }
    }
    //...
}

Controller 抛出异常时, DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常, 而 ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的 @ExceptionHandler 标注的方法是这里:

public class ExceptionHandlerMethodResolver {
    //...

    private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList();
        //根据异常类型, 查找匹配的异常处理⽅法 
        //⽐如NullPointerException会匹配两个异常处理⽅法: 
        //handler(Exception e) 和 handler(NullPointerException e) 
        for (Class<? extends Throwable> mappedException :
                this.mappedMethods.keySet()) {
            if (mappedException.isAssignableFrom(exceptionType)) {
                matches.add(mappedException);
            }
        }
        //如果查找到多个匹配, 就进⾏排序, 找到最使⽤的⽅法. 排序的规则依据抛出异常相对于声明异常的深度
        //⽐如抛出的是NullPointerException(继承于RuntimeException, RuntimeException又继承于Exception)
        //相对于handler(NullPointerException e) 声明的NullPointerException深度为0, 
        //相对于handler(Exception e) 声明的Exception 深度 为2 
        //所以 handler(NullPointerException e)标注的⽅法会排在前⾯ 
        if (!matches.isEmpty()) {
            if (matches.size() > 1) {
                matches.sort(new ExceptionDepthComparator(exceptionType));
            }
            return this.mappedMethods.get(matches.get(0));
        } else {
            return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
        }
    }
    //...
}

3.2 1.2中返回String会报错原因分析

SpringMVC 默认会注册一些自带的 HttpMessageConverter (从先后顺序怕排列分别为ByteArrayHttpMessageConverter,  StringHttpMessageConverter , SourceHttpMessageConverter, SourceHttpMessageConverter, AllEncompassingFormHttpMessageConverter)

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {

    //...
    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if (!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            } catch (Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
        }
        this.messageConverters.add(new
                AllEncompassingFormHttpMessageConverter());
    }
    //...
}

其中 AllEncompassingFormHttpMessageConverter 会根据项目依赖情况添加对应的 HttpMessageConverter

public AllEncompassingFormHttpMessageConverter() {
        if (!shouldIgnoreXml) {
            try {
                addPartConverter(new SourceHttpMessageConverter<>());
            } catch (Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
            if (jaxb2Present && !jackson2XmlPresent) {
                addPartConverter(new Jaxb2RootElementHttpMessageConverter());
            }
        }
        if (kotlinSerializationJsonPresent) {
            addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
        }
        if (jackson2Present) {
            addPartConverter(new MappingJackson2HttpMessageConverter());
        } else if (gsonPresent) {
            addPartConverter(new GsonHttpMessageConverter());
        } else if (jsonbPresent) {
            addPartConverter(new JsonbHttpMessageConverter());
        }
        if (jackson2XmlPresent && !shouldIgnoreXml) {
            addPartConverter(new MappingJackson2XmlHttpMessageConverter());
        }
        if (jackson2SmilePresent) {
            addPartConverter(new MappingJackson2SmileHttpMessageConverter());
        }
    }

在依赖中引入 jackson 包后, 容器会把 MappingJackson2HttpMessageConverter 自动注入到 messageConverters 链的末尾.

Spring 会根据返回的数据类型, 从 messageConverters 链选择合适的 HttpMessageConverter.

当返回的数据时非字符串时, 使用的 MappingJackson2HttpMessageConverter 写入返回对象.

当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到, 这时会认为 StringHttpMessageConverter 可以使用. 

public abstract class AbstractMessageConverterMethodProcessor extends
        AbstractMessageConverterMethodArgumentResolver
        implements HandlerMethodReturnValueHandler {

    //...代码省略
    protected <T> void writeWithMessageConverters(@Nullable T value,
                                                  MethodParameter returnType,
                                                  ServletServerHttpRequest inputMessage, ServletServerHttpResponse
                                                          outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException,
            HttpMessageNotWritableException {
        //...代码省略
        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter = (converter
                        instanceof GenericHttpMessageConverter ?
                        (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ?
                        ((GenericHttpMessageConverter)
                                converter).canWrite(targetType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {
                    //getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
                    结果
                            body = getAdvice().beforeBodyWrite(body, returnType,
                            selectedMediaType,
                            (Class<? extends HttpMessageConverter<?>>)
                                    converter.getClass(),
                            inputMessage, outputMessage);
                    if (body != null) {
                        Object theBody = body;
                        LogFormatUtils.traceDebug(logger, traceOn ->
                                "Writing [" + LogFormatUtils.formatValue(theBody,
                                        !traceOn) + "]");
                        addContentDispositionHeader(inputMessage, outputMessage);
                        if (genericConverter != null) {
                            genericConverter.write(body, targetType,
                                    selectedMediaType, outputMessage);
                        }
                        else {
                            //此时cover为StringHttpMessageConverter
                            ((HttpMessageConverter) converter).write(body,
                                    selectedMediaType, outputMessage);
                        }
                    }
                    else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Nothing to write: null body");
                        }
                    }
                    return;
                }
            }
        }
        //...代码省略

    }
    //...代码省略
}

在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中, 调用父类的 write 方法

由于 StringHttpMessageConverter 重写了 addDefaultHeaders 方法, 所以会执行子类的方法

然而子类 StringHttpMessageConverteraddDefaultHeaders 方法定义接收参数为 String, 此时 tResult类型, 所以出现类型不匹配 "Result cannot be cast to java.lang.String" 异常.

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏微凉.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值