关于java异常处理与开发体验和用户体验的思考(下)

简介: 关于java异常处理与开发体验和用户体验的思考

响应数据结构和异常类型统一后,我们需要统一处理controller的返回数据,全部包装成CommonResponse类型的数据。

import com.zx.eagle.annotation.IgnoreResponseAdvice;
import com.zx.eagle.vo.CommonResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Objects;
/** @author zouwei */
@RestControllerAdvice
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(
            MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        boolean ignore = false;
        IgnoreResponseAdvice ignoreResponseAdvice =
                returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
        if (Objects.nonNull(ignoreResponseAdvice)) {
            ignore = ignoreResponseAdvice.value();
            return !ignore;
        }
        Class<?> clazz = returnType.getDeclaringClass();
        ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
        if (Objects.nonNull(ignoreResponseAdvice)) {
            ignore = ignoreResponseAdvice.value();
        }
        return !ignore;
    }
    @Override
    public Object beforeBodyWrite(
            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response) {
        if (Objects.isNull(body)) {
            return CommonResponse.successInstance();
        }
        if (body instanceof CommonResponse) {
            return body;
        }
        CommonResponse commonResponse = CommonResponse.successInstance(body);
        return commonResponse;
    }
}
复制代码

很明显,并不是所有的返回对象都需要包装的,比如controller已经返回了CommonResponse,那么就不需要包装

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** @author zouwei */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreResponseAdvice {
    /**
     * 是否需要被CommonResponseAdvice忽略
     *
     * @return
     */
    boolean value() default true;
}
复制代码

其次,我们还需要统一处理异常

import com.google.common.collect.Lists;
import com.zx.eagle.common.config.ExceptionTipsStackConfig;
import com.zx.eagle.common.exception.EagleException;
import com.zx.eagle.common.exception.handler.ExceptionNotifier;
import com.zx.eagle.common.vo.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/** @author zouwei */
@Slf4j
@RestControllerAdvice
public class ExceptionResponseAdvice {
    @Autowired private ExceptionTipsStackConfig exceptionStack;
    @Autowired(required = false)
    private List<ExceptionNotifier> exceptionNotifierList;
    /**
     * 用户行为导致的错误
     *
     * @param e
     * @return
     */
    @ExceptionHandler(EagleException.class)
    public CommonResponse handleEagleException(
            EagleException e, HttpServletRequest request, HttpServletResponse response) {
        String massage = handleExceptionMessage(e);
        CommonResponse commonResponse =
                CommonResponse.exceptionInstance(e.getCode(), massage, e.getTips());
        sendNotify(e, request, response);
        return commonResponse;
    }
    /**
     * 处理未知错误
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public CommonResponse handleRuntimeException(
            RuntimeException e, HttpServletRequest request, HttpServletResponse response) {
        String error = handleExceptionMessage(e);
        EagleException unknownException = EagleException.unknownException(error);
        CommonResponse commonResponse =
                CommonResponse.exceptionInstance(
                        unknownException.getCode(), error, unknownException.getTips());
        sendNotify(unknownException, request, response);
        return commonResponse;
    }
    /**
     * 处理参数验证异常
     *
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public CommonResponse handleValidException(
            ConstraintViolationException e,
            HttpServletRequest request,
            HttpServletResponse response) {
        String error = handleExceptionMessage(e);
        Set<ConstraintViolation<?>> set = e.getConstraintViolations();
        Iterator<ConstraintViolation<?>> iterator = set.iterator();
        List<EagleException.ValidMessage> list = Lists.newArrayList();
        while (iterator.hasNext()) {
            EagleException.ValidMessage validMessage = new EagleException.ValidMessage();
            ConstraintViolation<?> constraintViolation = iterator.next();
            String message = constraintViolation.getMessage();
            Path path = constraintViolation.getPropertyPath();
            Object fieldValue = constraintViolation.getInvalidValue();
            String tipsKey = constraintViolation.getMessageTemplate();
            validMessage.setTipsKey(tipsKey);
            validMessage.setFieldName(path.toString());
            validMessage.setFieldValue(fieldValue);
            validMessage.setDefaultMessage(message);
            list.add(validMessage);
        }
        EagleException validException = EagleException.validException(list);
        sendNotify(validException, request, response);
        return CommonResponse.exceptionInstance(validException, error);
    }
    /**
     * 发送请求
     *
     * @param exception
     * @param request
     * @param response
     */
    private void sendNotify(
            EagleException exception, HttpServletRequest request, HttpServletResponse response) {
        if (!CollectionUtils.isEmpty(exceptionNotifierList)) {
            for (ExceptionNotifier notifier : exceptionNotifierList) {
                if (notifier.support(exception.getTipsKey())) {
                    notifier.handle(exception, request, response);
                }
            }
        }
    }
    /**
     * 处理异常信息
     *
     * @param e
     * @return
     */
    private String handleExceptionMessage(Exception e) {
        String massage = e.getMessage();
        String stackInfo = toStackTrace(e);
        String messageStackInfo = massage + "{" + stackInfo + "}";
        // 无论是否让客户端显示堆栈信息,后台都要记录
        log.error(messageStackInfo);
        if (exceptionStack.isShowMessage() && exceptionStack.isShowStack()) {
            return messageStackInfo;
        } else if (exceptionStack.isShowMessage()) {
            return massage;
        } else if (exceptionStack.isShowStack()) {
            return stackInfo;
        }
        return StringUtils.EMPTY;
    }
    /**
     * 获取异常堆栈信息
     *
     * @param e
     * @return
     */
    private static String toStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        try {
            e.printStackTrace(pw);
            return sw.toString();
        } catch (Exception e1) {
            return StringUtils.EMPTY;
        }
    }
}
复制代码

为了解决有一些异常需要额外处理的,例如调用第三方接口,接口返回异常并告知费用不够需要充值,这个时候就需要额外通知到相关人员及时充值。为此,特地添加一个接口:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 异常通知器
 *
 * @author zouwei
 */
public interface ExceptionNotifier {
    /**
     * 是否支持处理该异常
     *
     * @param exceptionKey
     * @return
     */
    boolean support(String exceptionKey);
    /**
     * 处理该异常
     *
     * @param e
     * @param request
     */
    void handle(EagleException e, HttpServletRequest request, HttpServletResponse response);
}
复制代码

为了满足返回的异常信息可配置化,通过配置决定不同的环境返回指定的字段信息

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** @author zouwei */
@Data
@Component
@ConfigurationProperties(prefix = "exception-tips-stack")
public class ExceptionTipsStackConfig {
    /** 是否显示堆栈信息 */
    private boolean showStack = false;
    /** 是否显示exception message */
    private boolean showMessage = false;
}
复制代码

application.yaml中配置示例(根据环境配置):

exceptionTipsStack:
  #异常堆栈是否需要显示
  showStack: true
  #开发提示信息是否需要显示
  showMessage: true
复制代码

为了保证返回的数据是指定的json格式,需要配置HttpMessageConverter

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/** @author zouwei */
@Configuration
public class CustomWebConfigure implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.clear();
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}
复制代码

4.测试

先将application.yaml调整为:

exceptionTipsStack:
  #异常堆栈是否需要显示
  showStack: true
  #开发提示信息是否需要显示
  showMessage: true
复制代码

编写TestController:

import com.zx.eagle.exception.EagleException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.*;
/** @author zouwei */
@Validated
@RestController
@RequestMapping("/test")
public class TestController {
    /**
     *
     * @return
     * @throws EagleException
     */
    @GetMapping("/user_repeat")
    public String userRepeat() throws EagleException {
        throw EagleException.newInstance("USER_REPEAT_REGIST", "用户重复注册了,正常提示");
    }
    /**
     * 对于用户来说,不应该直接看到NoSuchAlgorithmException,因为这并不是用户造成的,所以应该使用未知错误
     *
     * @return
     */
    @GetMapping("/unknownException")
    public String unknownException() throws EagleException {
        final MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD4");
        } catch (final NoSuchAlgorithmException e) {
            throw EagleException.unknownException("显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的");
        }
        return "success";
    }
  @GetMapping("/valid")
    public String validException(
            @NotNull(message = "USER_NAME_NOT_NULL")
                    @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT")
                    String username,
            @NotNull(message = "年龄不能为空")
                    @Min(value = 18, message = "年龄必须大于18岁")
                    @Max(value = 70, message = "年龄不能超过70岁")
                    int age)
            throws EagleException {
        // return "success";
        throw EagleException.newInstance("USER_NO_EXIST", "用户不存在,这个地方要注意");
    }
  @PostMapping("/valid4Post")
    public String validException2(@Valid @RequestBody User user, BindingResult result) {
        return "success";
    }
    @Data
    private static class User {
        @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT")
        private String username;
        @Min(value = 18, message = "年龄必须大于18岁")
        @Max(value = 70, message = "年龄不能超过70岁")
        private int age;
    }
}
复制代码

测试结果:

url: /test/user_repeat

{
  //错误码
  "code":"11023",
  //显示给开发人员看,方便调试
  //这个信息里面包括修复信息和异常的堆栈信息,包括异常出在InsuranceController.java:23,也就是这个类的第23行userRepeat方法里面
  "error":"用户重复注册了,正常提示{EagleException(code=11023, tips=重复注册)\n\tat com.zx.eagle.exception.EagleException.newInstance(EagleException.java:37)\n\tat com.zx.eagle.controller.InsuranceController.userRepeat(InsuranceController.java:23)\n}",
  //显示给用户看,明确错误,提示纠正措施
  "message":"重复注册",
  "data":null
}
复制代码

url: /test/unknownException

{
  //错误码
  "code":"-1",
  //告知出错原因,并给出修复提示,包含堆栈信息,帮助定位异常位置
  "error":"显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的{EagleException(code=-1, tips=未知错误)\n\tat com.zx.eagle.exception.EagleException.newInstance(EagleException.java:37)\n\tat com.zx.eagle.exception.EagleException.unknownException(EagleException.java:47)\n\tat com.zx.eagle.controller.InsuranceController.unknownException(InsuranceController.java:37)}",
  //显示给用户看,相对于无反应或直接展示异常信息更好
  "message":"未知错误",
  "data":null
}
复制代码

url:/test/valid?username=z2341d&age=10

{
code: "-2",
error: "test.age: 年龄必须大于18岁{javax.validation.ConstraintViolationException: test.age: 年龄必须大于18岁 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) }",
message: "参数验证错误",
validMessage: [
{
fieldName: "test.age",
fieldValue: 10,
code: "-2",
tips: "年龄必须大于18岁",
tipsKey: "年龄必须大于18岁",
defaultMessage: "年龄必须大于18岁"
}
],
data: null
}
复制代码

url:/test/valid4Post结果同上

至此,关于异常处理的相关思考和实现阐述完毕。小伙伴们可以依据类似的思考方式实现符合自身实际情况的异常处理方式。

欢迎有过类似思考的小伙伴一起讨论。


相关文章
|
24天前
|
消息中间件 人工智能 Java
抖音微信爆款小游戏大全:免费休闲/竞技/益智/PHP+Java全筏开源开发
本文基于2025年最新行业数据,深入解析抖音/微信爆款小游戏的开发逻辑,重点讲解PHP+Java双引擎架构实战,涵盖技术选型、架构设计、性能优化与开源生态,提供完整开源工具链,助力开发者从理论到落地打造高留存、高并发的小游戏产品。
|
24天前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
43 0
|
2月前
|
安全 Java 数据库
Java 项目实战病人挂号系统网站设计开发步骤及核心功能实现指南
本文介绍了基于Java的病人挂号系统网站的技术方案与应用实例,涵盖SSM与Spring Boot框架选型、数据库设计、功能模块划分及安全机制实现。系统支持患者在线注册、登录、挂号与预约,管理员可进行医院信息与排班管理。通过实际案例展示系统开发流程与核心代码实现,为Java Web医疗项目开发提供参考。
111 2
|
2月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
148 3
|
2月前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
204 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
3月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
148 1
|
3月前
|
安全 Java API
Java 17 及以上版本核心特性在现代开发实践中的深度应用与高效实践方法 Java 开发实践
本项目以“学生成绩管理系统”为例,深入实践Java 17+核心特性与现代开发技术。采用Spring Boot 3.1、WebFlux、R2DBC等构建响应式应用,结合Record类、模式匹配、Stream优化等新特性提升代码质量。涵盖容器化部署(Docker)、自动化测试、性能优化及安全加固,全面展示Java最新技术在实际项目中的应用,助力开发者掌握现代化Java开发方法。
137 1
|
3月前
|
IDE Java API
Java 17 新特性与微服务开发的实操指南
本内容涵盖Java 11至Java 17最新特性实战,包括var关键字、字符串增强、模块化系统、Stream API、异步编程、密封类等,并提供图书管理系统实战项目,帮助开发者掌握现代Java开发技巧与工具。
180 1
|
2月前
|
移动开发 Cloud Native 安全
Java:跨平台之魂,企业级开发的磐石
Java:跨平台之魂,企业级开发的磐石

热门文章

最新文章