如何以编程方式将Spring的NumberFormatException替换为用户友好的文本?

16

我正在开发一个Spring web应用程序,我有一个实体,其中包含一个整数属性。当用户使用JSP表单创建新实体时,可以填写该属性。下面是由此表单调用的控制器方法:

@RequestMapping(value = {"/newNursingUnit"}, method = RequestMethod.POST)
public String saveNursingUnit(@Valid NursingUnit nursingUnit, BindingResult result, ModelMap model) 
{
    boolean hasCustomErrors = validate(result, nursingUnit);
    if ((hasCustomErrors) || (result.hasErrors()))
    {
        List<Facility> facilities = facilityService.findAll();
        model.addAttribute("facilities", facilities);

        setPermissions(model);

        return "nursingUnitDataAccess";
    }

    nursingUnitService.save(nursingUnit);
    session.setAttribute("successMessage", "Successfully added nursing unit \"" + nursingUnit.getName() + "\"!");
    return "redirect:/nursingUnits/list";
}

validate方法仅检查名称是否已存在于数据库中,因此我没有包含它。我的问题是,当我故意在字段中输入文本时,我希望能够得到一条好的消息,例如“自动放电时间必须是数字!”。但是,Spring返回这个绝对可怕的错误:

Failed to convert property value of type [java.lang.String] to required type [java.lang.Integer] for property autoDCTime; nested exception is java.lang.NumberFormatException: For input string: "sdf"

我完全理解为什么会发生这种情况,但我无论如何都想不出如何以编程方式替换Spring的默认数字格式异常错误消息。我知道可以使用消息源来实现此类事情,但我真的想直接在代码中实现这一点。

编辑

如建议所述,我在我的控制器中构建了此方法,但仍然收到Spring的“无法转换属性值...”消息:

@ExceptionHandler({NumberFormatException.class})
private String numberError()
{
   return "The auto-discharge time must be a number!";
}

其他编辑

这里是我的实体类代码:

@Entity
@Table(name="tblNursingUnit")
public class NursingUnit implements Serializable 
{
private Integer id;
private String name;
private Integer autoDCTime;
private Facility facility;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() 
{
    return id;
}

public void setId(Integer id) 
{
    this.id = id;
}

@Size(min = 1, max = 15, message = "Name must be between 1 and 15 characters long")
@Column(nullable = false, unique = true, length = 15)
public String getName() 
{
    return name;
}

public void setName(String name) 
{
    this.name = name;
}

@NotNull(message = "The auto-discharge time is required!")
@Column(nullable = false)
public Integer getAutoDCTime() 
{
    return autoDCTime;
}

public void setAutoDCTime(Integer autoDCTime) 
{
    this.autoDCTime = autoDCTime;
}

@ManyToOne (fetch=FetchType.EAGER)
@NotNull(message = "The facility is required")
@JoinColumn(name = "id_facility", nullable = false)
public Facility getFacility()
{
    return facility;
}

public void setFacility(Facility facility)
{
    this.facility = facility;
}

@Override
public boolean equals(Object obj) 
{
    if (obj instanceof NursingUnit)
    {
        NursingUnit nursingUnit = (NursingUnit)obj;
        if (Objects.equals(id, nursingUnit.getId()))
        {
            return true;
        }
    }
    return false;
}

@Override
public int hashCode() 
{
    int hash = 3;
    hash = 29 * hash + Objects.hashCode(this.id);
    hash = 29 * hash + Objects.hashCode(this.name);
    hash = 29 * hash + Objects.hashCode(this.autoDCTime);
    hash = 29 * hash + Objects.hashCode(this.facility);
    return hash;
}

@Override
public String toString()
{
    return name + " (" + facility.getCode() + ")";
}
}

又一次的编辑

我可以通过在类路径上包含以下内容的message.properties文件使其工作:

typeMismatch.java.lang.Integer={0} must be a number!

并且在配置文件中有以下的bean声明:

@Bean
public ResourceBundleMessageSource messageSource() 
{
    ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
    resource.setBasename("message");
    return resource;
}

这让我收到了正确的错误信息,而不是Spring的通用"TypeMismatchException" / "NumberFormatException",虽然我可以容忍但仍然希望尽可能在程序中处理所有事情,并且一直在寻找替代方案。

感谢您的帮助!


当然。我正在尽可能地使我的应用程序防错,因此我试图使服务器返回的错误信息更友好一些,当有人错误地(或者像我这样故意地)在一个与整数值“映射”到的字段中输入文本时。 - Martin
好的,我猜到了。我建议的解决方案也在官方文档中描述,应该可以工作。我会更新答案并附上链接,也许可以帮助您理解为什么它不起作用。 - NiVeR
谢谢,我会阅读这篇文章,看看是否能找出我做错的地方。 - Martin
@Martin 你好,Martin,你使用的Spring版本是哪个? - LoolKovsky
目前使用的版本是5.0.4,不过我计划在不久的将来升级到最新版本。 - Martin
实际上,现在的版本是5.0.7,这是最新版本。 - Martin
5个回答

3

谢谢您的答复!假设我在我的message.properties文件中拥有以下内容,并且目前一切正常:typeMismatch.nursingUnit.autoDCTime=自动出院时间必须是一个数字!如何复制返回这个错误消息?我不确定是否理解,因为该方法返回一个字符串数组? - Martin
在您的情况下,您只需要返回一个包含所需错误的字符串值。如果字段失败多个检查并且您需要多个消息,则为数组。您可能需要向字段添加注释,以便您可以获得注释验证失败而不是转换失败。我不确定转换失败。我确定它会在注释验证失败时被调用。那里的消息格式看起来与注释相同。在您的情况下,您可能只需检查字段,如果匹配AutoDCTime,则返回您的错误消息。 - Joe W
我按照你建议构建了一个类,用 @Component 进行了注释,并确保它位于扫描包中。我在你提到的方法中添加了一个断点,但当我尝试验证表单中的 autoDCTime 字段中的非数字时,该断点从未被触发。 - Martin
在更多的阅读中,看起来DefaultmessageCodesResolver只适用于类型检查之后发生的验证注释。我认为这将需要覆盖DefaultBindingErrorProcessor。更新了答案,并思考了下面的想法:最终你手头持有的那个message.properties可能是最好的选择。 - Joe W

2
您可以使用以下方式注释一个方法:
@ExceptionHandler({NumberFormatException.class})
public String handleError(){
   //example
   return "Uncorrectly formatted number!";
}

当抛出该类型的异常时,您可以实现任何想要执行的操作。给定的代码将处理当前控制器中发生的异常。有关更多参考,请查阅此链接

要进行全局错误处理,您可以按以下方式使用@ControllerAdvice

@ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {

   @ExceptionHandler({NumberFormatException.class})
    public String handleError(){
       //example
       return "Uncorrectly formatted number!";
    }
} 

我记得尝试过类似的事情,但我不知道实际该放什么。 当我生成自定义错误时,我会像这样做:FieldError error = new FieldError(“nursingUnit”、“name”、nursingUnit.getName()、false、null、null、 nursingUnit.getName() + “已存在!”); 那么在该方法中我应该放什么代码来告诉Spring将其丑陋的错误替换为我的版本? - Martin
autoDCTime属性附加在由我的saveNursingUnit方法验证的NursingUnit对象上。这个方法应该放在控制器中还是实体类本身中? - Martin
@Martin 如果您将新异常处理方法修改为public而不是private,您是否看到任何变化? - takendarkk
将方法从私有改为公共的操作,遗憾地没有任何效果。 - Martin
1
@ExceptionHandler 只适用于发生在你的控制器或其调用的任何内容内部的错误。异常被抛出是因为它无法将 POST 映射到你的控制器输入,而这是在你的代码之外发生的。 - Jean Marois
显示剩余5条评论

0

@Martin,我问你版本的原因是因为@ControllerAdvice从3.2版本开始可用。

我建议您使用@ControllerAdvice,这是一个注释,允许您编写可在控制器(带有@Controller@RestController注释)之间共享的代码,但它也可以仅应用于特定包或具体类中的控制器。

ControllerAdvice旨在与@ExceptionHandler@InitBinder@ModelAttribute一起使用。

您可以像这样设置目标类:@ControllerAdvice(assignableTypes = {YourController.class, ...})

@ControllerAdvice(assignableTypes = {YourController.class, YourOtherController.class})
public class YourExceptionHandler{
    //Example with default message
    @ExceptionHandler({NumberFormatException.class})
    private String numberError(){
        return "The auto-discharge time must be a number!";
    }

    //Example with exception handling
    @ExceptionHandler({WhateverException.class})
    private String whateverError(WhateverException exception){
        //do stuff with the exception
        return "Whatever exception message!";
    }

    @ExceptionHandler({ OtherException.class })
    protected String otherException(RuntimeException e, WebRequest request) {
        //do stuff with the exception and the webRequest
        return "Other exception message!";
    }
} 

需要记住的是,如果您没有设置目标并且在不同的@ControllerAdvice类中为相同的异常定义了多个异常处理程序,则Spring将应用它找到的第一个处理程序。如果在同一个@ControllerAdvice类中存在多个异常处理程序,则会抛出错误。

@Martin,抱歉晚发了,我昨天就准备好了,但一直没能发布。我还添加了一些小例子,展示如何扩展“ExceptionHandler”注释以应对可能需要使用请求或异常本身的不同情况。 - LoolKovsky
这个答案和我的一模一样(或者非常相似)。 - NiVeR

0

解决方案 1: StaticMessageSource 作为 Spring bean

这样我就可以得到正确的错误信息,而不是 Spring 的通用 TypeMismatchException / NumberFormatException。虽然我可以容忍它们,但我仍然想尽可能地在代码中完成所有操作,并寻找替代方案。

您的示例使用了ResourceBundleMessageSource,该类使用资源包(如属性文件)。如果您想要完全以编程方式完成所有操作,则可以使用一个StaticMessageSource。您可以将其设置为Spring bean,命名为messageSource。例如:

@Configuration
public class TestConfig {
    @Bean
    public MessageSource messageSource() {
        StaticMessageSource messageSource = new StaticMessageSource();
        messageSource.addMessage("typeMismatch.java.lang.Integer", Locale.getDefault(), "{0} must be a number!");
        return messageSource;
    }
}

这是获取用户友好消息的最简单解决方案。

(确保名称为messageSource。)

解决方案2:自定义BindingErrorProcessor用于initBinder

此解决方案比解决方案1更低级且不太容易,但可能会给您更多控制:

public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {
    public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
        Throwable cause = ex.getCause();
        if (cause instanceof NumberFormatException) {
            String field = ex.getPropertyName();
            Object rejectedValue = ex.getValue();
            String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
            Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field);

            boolean useMyOwnErrorMessage = true; // just so that you can easily see to default behavior one line below
            String message = useMyOwnErrorMessage ? field + " must be a number!" : ex.getLocalizedMessage();
            FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, codes, arguments, message);
            error.wrap(ex);
            bindingResult.addError(error);
        } else {
            super.processPropertyAccessException(ex, bindingResult);
        }
    }
}

@ControllerAdvice
public class MyControllerAdvice {
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        BindingErrorProcessor customBindingErrorProcessor = new CustomBindingErrorProcessor();
        binder.setBindingErrorProcessor(customBindingErrorProcessor);
    }
}

它基本上拦截了对DefaultBindingErrorProcessor.processPropertyAccessException的调用,并在绑定失败时添加自定义的FieldError消息,当出现NumberFormatException时。

没有Spring Web/MVC的示例代码

如果您想尝试不使用Spring Web/MVC,而只是纯Spring,则可以使用此示例代码。

public class MyApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        Validator validator = context.getBean(LocalValidatorFactoryBean.class);

        // Empty person bean to be populated
        Person2 person = new Person2(null, null);
        // Data to be populated
        MutablePropertyValues propertyValues = new MutablePropertyValues(List.of(
                new PropertyValue("name", "John"),
                // Bad value
                new PropertyValue("age", "anInvalidInteger")
        ));

        DataBinder dataBinder = new DataBinder(person);
        dataBinder.setValidator(validator);
        dataBinder.setBindingErrorProcessor(new CustomBindingErrorProcessor());

        // Bind and validate
        dataBinder.bind(propertyValues);
        dataBinder.validate();

        // Get and print results
        BindingResult bindingResult = dataBinder.getBindingResult();
        bindingResult.getAllErrors().forEach(error -> 
                System.out.println(error.getDefaultMessage())
        );
        
        // Output:
        // "age must be a number!"
    }
}

@Configuration
class MyConfig {
    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

class Person2 {
    @NotEmpty
    private String name;

    @NotNull @Range(min = 20, max = 50)
    private Integer age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public Person2(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}


-2

处理 NumberFormatException 异常。

try {
 boolean hasCustomErrors = validate(result, nursingUnit);
}catch (NumberFormatException nEx){
 // do whatever you want
 // for example : throw custom Exception with the custom message.
}

validate方法是我自己编写的,不会抛出任何异常。丑陋的错误是由Spring本身生成的,我正在寻找一种替换它的方法。 - Martin
你认为Spring会抛出NumberFormatException吗?不会。 - Sundararaj Govindasamy
这个错误绝对不是由我编写的代码引起的,我认为它确实是由Spring抛出的,当尝试将用户错误输入的字符串数据转换为我的实体类所需的整数类型时。 - Martin
3
很遗憾,你的回答完全没有理解我提出的问题,否则你会很可信。我的验证方法不会抛出NumberFormatException异常,它只会在数据库中已存在姓名字段时向模型添加一个FieldError。 - Martin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接