关于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结果同上

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

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


相关文章
|
4天前
|
JavaScript 安全 Java
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
31 12
|
27天前
|
前端开发 Java 程序员
菜鸟之路day02-04拼图小游戏开发一一JAVA基础综合项目
本项目基于黑马程序员教程,涵盖面向对象进阶、继承、多态等知识,历时约24小时完成。项目去除了登录和注册模块,专注于单机游戏体验。使用Git进行版本管理,代码托管于Gitee。项目包含窗体搭建、事件监听、图片加载与打乱、交互逻辑实现、菜单功能及美化界面等内容。通过此项目,巩固了Java基础并提升了实际开发能力。 仓库地址:[https://gitee.com/zhang-tenglan/puzzlegame.git](https://gitee.com/zhang-tenglan/puzzlegame.git)
42 6
|
30天前
|
前端开发 Java 数据库连接
【潜意识Java】深度解读JavaWeb开发在Java学习中的重要性
深度解读JavaWeb开发在Java学习中的重要性
30 4
|
30天前
|
Java 应用服务中间件 API
【潜意识Java】javaee中的SpringBoot在Java 开发中的应用与详细分析
本文介绍了 Spring Boot 的核心概念和使用场景,并通过一个实战项目演示了如何构建一个简单的 RESTful API。
41 5
|
30天前
|
SQL Java API
|
30天前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
55 2
|
30天前
|
Java 数据库连接 数据处理
探究Java异常处理【保姆级教程】
Java 异常处理是确保程序稳健运行的关键机制。它通过捕获和处理运行时错误,避免程序崩溃。Java 的异常体系以 `Throwable` 为基础,分为 `Error` 和 `Exception`。前者表示严重错误,后者可细分为受检和非受检异常。常见的异常处理方式包括 `try-catch-finally`、`throws` 和 `throw` 关键字。此外,还可以自定义异常类以满足特定需求。最佳实践包括捕获具体异常、合理使用 `finally` 块和谨慎抛出异常。掌握这些技巧能显著提升程序的健壮性和可靠性。
47 4
|
1月前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
2月前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
1426 1
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
2月前
|
人工智能 自然语言处理 Java
重塑 Java 开发体验:SoFlu-JavaAI 产品体验官招募!100% 有礼!
Java 作为历史悠久且广泛应用的编程语言,在企业级应用、Web 开发、移动应用(尤其是 Android)及大数据处理等领域占据重要地位。其成熟稳定的特性和庞大的生态系统吸引了众多开发者,但也带来了高学习成本、复杂开发流程和竞争激烈等问题。为解决这些挑战,SoFlu-JavaAI 应运而生。这是一款基于大模型技术的 AI 开发工具,能通过自然语言对话自动生成 Java Maven 工程代码,提供引导式开发、一键工程构建与迭代支持等功能,大幅提高开发效率并减轻设计焦虑。目前,SoFlu-JavaAI 正在招募产品体验官,参与即可获得专属礼品和技术交流机会。

热门文章

最新文章