SpringMVC的消息转换机制

Java知识点总结:想看的可以从这里进入

2.3、消息转换机制

我们在使用SpringMVC是经常使用 JSON 格式的数据在前后端的之间进行传递,我们会发现在接收到前端传递来的JSON数据时,Spring会自动将这种格式的数据转换成对象的集合、entiy类等等。而这就是利用消息转换机制来完成的。

软件开发的实质就是前后端的数据交互,所以在数据验证上也是开发的一样重要工作。SpringMVC在这上面也内置了好几个组件来解决不同的问题。

  • Converter类型转换器:先获取到请求发送过来的参数,并将其转换为控制器方法指定的数据类型,然后再将转换后的参数值传递给控制器方法的形参。
  • Formatter格式化数据:将数据转化为一定的格式(比如:金钱¥100000、日期yyyy-MM-dd 等)
  • Validator 验证器:作用于对象级,决定某一个对象中的所有字段是否均是有效的

Spring 最早只是通过 HttpMessageConverter 对消息进行简易的转换,主要是转换成字符串,后来为了能将消息转换成POJO类或更多的数据类型,在 Spring4 后又提供了 转换器和格式化器,它能把HTTP 发送的各种消息转换成控制器所需的参数,在转换器和格式化器处理完参数后,就会使用验证器对数据进行验证,完成这些工作后,就会调用控制器,将转化验证好的参数传递进来。

image-20220916140429156
2.3.1、HttpMessageConverter

org.springframework.http.converter.HttpMessageConverter主要为了应对HTTP请求,用于在 HTTP 请求和响应之间进行转换的策略接口。它面向的是消息体,它只是对消息体中的数据进行初步的处理。

public interface HttpMessageConverter<T> {
	//指示此转换器是否可以读取给定的类。
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    //指示给定的类是否可以由此转换器写入
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    //返回此转换器支持的媒体类型列表。
	List<MediaType> getSupportedMediaTypes();
    //返回此转换器为给定类支持的媒体类型列表。
	default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
		return (canRead(clazz, null) || canWrite(clazz, null) ?
				getSupportedMediaTypes() : Collections.emptyList());
	}
	//从给定的输入消息中读取给定类型的对象,并返回它。
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;
    //将给定的对象写入给定的输出消息。
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

HttpMessageConverter属于顶层的接口,它有众多的实现类:

image-20230303152002322

其中我们使用较多的实现类如:MappingJackson2HttpMessageConverter,它用来处理JSON相关的转换类,通过此类能把控制器返回的数据转换成JSON格式。使用它需要先配置:

<!--配置JSON格式的转换器-->
<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    <property name="supportedMediaTypes">
        <list>
            <value>application/json;charset-UTF-8</value>
        </list>
    </property>
</bean>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="messageConverters">
        <list>
            <ref bean="jsonConverter"/>
        </list>
    </property>
</bean>

然后导入jackson 的依赖

<!--Jackson的依赖-->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.4</version>
</dependency>

配置好后我们就可以直接使用了,只要Controller中使用了@ResponseBody注解的方法,返回的对象都会转换成JSON格式

@RequestMapping("user")
@ResponseBody
public User user(){
    return new User("张三","中国","22","描述");
}
image-20220916144045833

返回的集合也已转化成Json数据

@RequestMapping("/testJson")
@ResponseBody
public List<User> testJson(Model model){
    List<User> users = new ArrayList<>();
    users.add(new User(1,"张三",20,"中国"));
    users.add(new User(1,"李四",20,"中国"));
    users.add(new User(1,"王五",20,"中国"));
    users.add(new User(1,"赵六",20,"中国"));
    return users;
}
image-20230301205417076
2.3.2、类型转换器

Converter(类型转换器),是 Spring MVC提供的一种数据类型转换工具 ,它的作用是在控制器方法执行前对请求参数进行处理,先获取到请求发送过来的参数,并将其转换为控制器方法指定的数据类型,然后再将转换后的参数值传递给控制器方法的形参,这样后台的控制器方法就可以正确地获取请求中携带的参数了。

在Spring中类型转换器有两个分支需要了解

  1. Converter:进行类型转换的相关接口,一般分为三种
    • Converter<S, T>:一对一,将一个类型S转换成另一个类型T,使用最简单,最不灵活;
    • ConverterFactory:它基于Converter,这个实质上就是Converter工厂,将多个Converter封装到一起,可以实现1对多的转换。使用较复杂,比较灵活
    • GenericConverter:多对多,数组集合之间进行转换。使用最复杂,也最灵活;
  2. ConversionService:用于类型转换的服务接口,是进入转换系统的入口点。
1、Converter

org.springframework.core.convert.converter.Converter是一种一对一的转换器,在 Spring3 中引入,内部有一个convert 方法,它是将原类型对象作为参数传入进行转换之后返回目标类型对象。当我们需要建立自己的 converter 的时候就可以实现该接口。

//转换器将类型S的源对象转换为类型T的目标。该接口的实现是线程安全的并且可以共享
@FunctionalInterface
public interface Converter<S, T> {	
    //将类型S的源对象转换为目标类型T 。
    @Nullable
    T convert(S source);
}

其实现类也非常多,但大多数都是以内部类的形式实现(@FunctionalInterface注解的接口),这些都是Spring内部自己使用的。

实现类作用
StringToBooleanConverterString 转为 boolean
StringToNumberConverterFactoryString 转为 数字
StringToCharacterConverterString 转为 Character (取字符串中的第一个字符)
StringToEnumConverterFactoryString 转为 枚举类型,通过 Enum.valueOf 将字符串转换为需要的枚举类型
StringToPropertiesConverterString 到 java.util.Properties 转换,默认使用 ISO-8859-1 编码
StringToLocaleConverterString 转为 java.util.Locale
ObjectToStringConverterObject 转为String 转换,调用 toString 方法转换
NumberToNumberConverterFactory数字子类型(基本类型)到数字类型(包装类型)转换
NumberToCharacterConverter数字子类型到 Character 转换
CharacterToNumberFactoryCharacter 到数字子类型转换
EnumToStringConverter枚举类型到 String 转换,返回枚举对象的 name 值
PropertiesToStringConverterjava.util.Properties 到 String 转换,默认通过 ISO-8859-1 解码
2、ConverterFactory

就是Converter工厂,可以将对象从S转换为R的子类型,属于1对多的转换。可以把传递进来的数据类型,转换成多个数据类型。

//“范围”转换器的工厂,可以将对象从 S 转换为 R 的子类型。
public interface ConverterFactory<S, R> {
	//获取转换器以将 S 转换为目标类型 T,其中 T 也是 R 的一个实例。
	<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

image-20230303153140352

3、数组集合转换器

Spring中还有一种多对多的转换,用于两个或多个类型之间的转换。它是最灵活也是最复杂的一种转换器。它位于:org.springframework.core.convert.GenericConverter。

public interface GenericConverter {
	//返回可接受的转换类型
    @Nullable
    Set<ConvertiblePair> getConvertibleTypes();
	//转换方法
    @Nullable
    Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
	//可转换匹配类
    final class ConvertiblePair {
        //源类型
        private final Class<?> sourceType;
		//目标类型
        private final Class<?> targetType;
		//创建一个新的源对象到目标类型
        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
            Assert.notNull(sourceType, "Source type must not be null");
            Assert.notNull(targetType, "Target type must not be null");
            this.sourceType = sourceType;
            this.targetType = targetType;
        }
        public Class<?> getSourceType() {
            return this.sourceType;
        }
        public Class<?> getTargetType() {
            return this.targetType;
        }
        @Override
        public boolean equals(@Nullable Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || other.getClass() != ConvertiblePair.class) {
                return false;
            }
            ConvertiblePair otherPair = (ConvertiblePair) other;
            return (this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType);
        }
        @Override
        public int hashCode() {
            return (this.sourceType.hashCode() * 31 + this.targetType.hashCode());
        }
        @Override
        public String toString() {
            return (this.sourceType.getName() + " -> " + this.targetType.getName());
        }
    }
}
名称作用
ArrayToCollectionConverter任意数组到任意集合(List、Set)转换
CollectionToArrayConverter任意集合到任意数组转换
ArrayToArrayConverter任意数组到任意数组转换
CollectionToCollectionConverter集合之间的类型转换
MapToMapConverterMap之间的类型转换
ArrayToStringConverter任意数组到 String 转换
StringToArrayConverter字符串到数组的转换,默认通过“,”分割,且去除字符串两边的空格(trim)
ArrayToObjectConverter任意数组到 Object 的转换,如果目标类型和源类型兼容,直接返回源对象;否则返回数组的第一个元素并进行类型转换
ObjectToArrayConverterObject 到单元素数组转换
CollectionToStringConverter任意集合(List、Set)到 String 转换
StringToCollectionConverterString 到集合(List、Set)转换,默认通过“,”分割,且去除字符串两边的空格(trim)
CollectionToObjectConverter任意集合到任意 Object 的转换,如果目标类型和源类型兼容,直接返回源对象;否则返回集合的第一个元素并进行类型转换
ObjectToCollectionConverterObject 到单元素集合的类型转换
4、ConversionService

关于类型转换器的注册这里有两个接口:ConversionService(类型转换的入口) 和 ConverterRegistry(用于注册类型转换器)。一般而言:我们在实现ConversionService接口的时候也会实现ConverterRegistry接口。

ConversionService是类型转换的服务接口,是转换系统的入口点,Spring就是通过这个接口 调用 其convert(Object, Class)以执行线程安全类型转换的。如果我们自定义转换器时,就需要在XML中将自定义的转换器注册到 conversionService 中才能生效。

public interface ConversionService {
    //如果sourceType的对象可以转换为targetType则返回true 。如果此方法返回true ,则意味着convert(Object, Class)能够将sourceType的实例转换为targetType 。
	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
    //如果sourceType的对象可以转换为targetType则返回true 。 TypeDescriptors 提供了有关发生转换的源位置和目标位置的附加上下文,通常是对象字段或属性位置。如果此方法返回true ,则意味着convert(Object, TypeDescriptor, TypeDescriptor)能够将sourceType的实例转换为targetType 。
    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    //将给定的source转换为指定的targetType 。
    <T> T convert(@Nullable Object source, Class<T> targetType);
    //给定的source转换为指定的targetType 。 TypeDescriptors 提供有关将发生转换的源位置和目标位置的附加上下文,通常是对象字段或属性位置。
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

image-20230303161917122

还有一个 ConverterRegistry接口 ,可以使我们对类型转换器做一个统一的注册。ConverterRegistry接口就分别为三种类型提供了对应的注册方法。

//用于向类型转换系统注册转换器。
public interface ConverterRegistry {
	//将普通转换器添加到此注册表。可转换的源/目标类型对派生自转换器的参数化类型。
	void addConverter(Converter<?, ?> converter);

	//将普通转换器添加到此注册表。明确指定可转换的源/目标类型对。
	<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);

	//将GenericConverter添加到此注册表
	void addConverter(GenericConverter converter);

	//将ConverterFactory添加到此注册表。
	void addConverterFactory(ConverterFactory<?, ?> factory);

	//删除从sourceType到targetType所有转换器。
	void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
image-20230303162655602
  1. 它们的子接口中都有ConfigurableConversionService 这样一个接口,它只是用于将两个接口合并到一起,没有任何新增加的方法。

    public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {}
    
  2. 其实现类 GenericConversionService 可以适用于大多数环境的基础ConversionService实现。但我们一般不直接使用,而是使用其子类:DefaultConversionService,它几乎覆盖注册了所有的通用的类型转换。

    public class DefaultConversionService extends GenericConversionService {
        public static void addDefaultConverters(ConverterRegistry converterRegistry) {
    		addScalarConverters(converterRegistry);
    		addCollectionConverters(converterRegistry);
    		converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
    		converterRegistry.addConverter(new StringToTimeZoneConverter());
    		converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
    		converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
    		converterRegistry.addConverter(new ObjectToObjectConverter());
    		converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
    		converterRegistry.addConverter(new FallbackObjectToStringConverter());
    		converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
    	}
        public static void addCollectionConverters(ConverterRegistry converterRegistry) {
    		ConversionService conversionService = (ConversionService) converterRegistry;
    		converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
    		converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
    		converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
    		converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
    		converterRegistry.addConverter(new MapToMapConverter(conversionService));
    		converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
    		converterRegistry.addConverter(new StringToArrayConverter(conversionService));
    		converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
    		converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));
    		converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
    		converterRegistry.addConverter(new StringToCollectionConverter(conversionService));
    		converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
    		converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
    		converterRegistry.addConverter(new StreamConverter(conversionService));
    	}
    	private static void addScalarConverters(ConverterRegistry converterRegistry) {
    		converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
    		converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
    		converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverter(new StringToCharacterConverter());
    		converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverter(new NumberToCharacterConverter());
    		converterRegistry.addConverterFactory(new CharacterToNumberFactory());
    		converterRegistry.addConverter(new StringToBooleanConverter());
    		converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
    		converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry));
    		converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
    		converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry));
    		converterRegistry.addConverter(new StringToLocaleConverter());
    		converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverter(new StringToCharsetConverter());
    		converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverter(new StringToCurrencyConverter());
    		converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());
    		converterRegistry.addConverter(new StringToPropertiesConverter());
    		converterRegistry.addConverter(new PropertiesToStringConverter());
    		converterRegistry.addConverter(new StringToUUIDConverter());
    		converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
    	}
    }
    
  3. FormattingConversionService:不仅有类型转换的功能,还有格式化转换的功能。实际上就是把类型转换器和格式转换器放到一起。它还有一个专门的工厂类 FormattingConversionServiceFactroyBean ,用来创造 FormattingConversionService 实例。

    具体的看格式化转换器

    public class FormattingConversionService extends GenericConversionService
    		implements FormatterRegistry, EmbeddedValueResolverAware {
    }
    
5、自定义转换器

Spring MVC 内置的类型转换器基本可以满足我们日常的开发需求,不过有些时候可能还是要自已定义一些数据格式,如果我们想要自定义类型转换器,就是创建一个转换器然后实现以类型转换器三种接口中的任何一个即可。创建完成后需要在XML中配置,显示的说明要使用自定义的类型转换器,这样才能生效,否则只会使用spring内部的转换器。

// 把一定格式的字符串(如:张三,23,中国) 转换 为User对象
public class PersonConverter implements Converter<String, Person> {

    @Override
    public Person convert(String s) {
        if (StringUtils.isEmpty(s)) {
            return null;
        }
        //将(张三,23,中国)这种格式的字符串进行分割
        String[] strings = StringUtils.delimitedListToStringArray(s, ",");
        Person person = new Person();
        person.setName(strings[0]);
        person.setAge(Integer.valueOf(strings[1]));
        person.setAddress(strings[2]);
        return person;
    }
}

需要将自定义的转换器通过XML配置,让其生效。而自定义的转换器需要 ConversionServiceFactoryBean工厂 提供入口。

<!--显式地装配自定义类型转换器-->
<mvc:annotation-driven conversion-service="conversionService"/>
<!--自定义类型转换器配置-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <!--加入自定义的转换器-->
            <bean class="com.yu.springmvc.converter.PersonConverter"></bean>
        </set>
    </property>
</bean>
2.3.3、格式化转换器

像日期、金额这种类型的数据,都需要转换成特定的格式才行。日期 yyyy-MM-dd、yyyy-MM-dd hh:ss:mm等格式,金额10000转成 ¥10 000.00。

1、Formatter

org.springframework.format.Spring为了这些场景提供了 Formatter 接口, T 表示目标数据类型,它被称为格式化转换器。在其基础上还有2个扩展接口,Printer(将数据按照一定的格式输出成字符串)、Parser(将满足一定格式的字符串转换为对象)。

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

@FunctionalInterface
public interface Printer<T> {
	/** 打印 T 类型的对象以供显示。*/
	String print(T object, Locale locale);

}

@FunctionalInterface
public interface Parser<T> {
	/** 解析文本字符串以生成 T */
	T parse(String text, Locale locale) throws ParseException;

}

在Spring MVC 默认提供了多个内置的格式化器,通过它们,我们可以轻松地实现对日期(Date)类型和数值(Number)类型数据的格式化工作,基本不需要我们自己自定义。

内置格式化器说明
InstantFormatter时间戳的转换和解析
NumberStyleFormatterNumber 与 String 之间的解析与格式化。
CurrencyFormatterNumber 与 String 之间的解析与格式化(带货币符号)。
CurrencyStyleFormatter以BigDecimal的格式来处理数字,当作钱币处理。
PercentFormatterNumber 与 String 之间的解析与格式化(带百分数符号)。
DateFormatter实现 Date 与 String 之间的解析与格式化。(比较重要的一个实现类)
AbstractNumberFormatter对 java.lang.Number进行格式化。

其中有两个注解需要注意一下:

  1. @DateTimeFormat (实现对日期类型数据的格式化)
    1. pattern:用于指定解析或格式化日期时间的模式,其常用取值包括 yyyy-MM-dd、yyyy-MM-dd hh:mm:ss
    2. iso:用于指定解析或格式化日期时间的 ISO 模式
      • DATE:yyyy-MM-dd
      • TIME:hh:mm:ss:SSSZ
      • DATE_TIME:yyyy-MM-dd hh:mm:ss:SSSZ
      • NONE:无
    3. style:用于指定日期时间的格式。第一个字符表示日期的格式,第二个字符表示时间的格式。默认值SS
      • S:短日期/时间格式
      • M:中日期/时间格式
      • L:长日期/时间格式
      • F:完整日期/时间格式
  2. @NumberFormat (实现对数值类型数据的格式化)。
    1. style:用于指定数值的样式类型,其取值有以下 4 种:
      • DEFAULT:默认类型
      • NUMBER:正常数值类型
      • PERCENT:百分数类型
      • CURRENCY:货币数值类型
    2. pattern:用于自定义数值的样式,例如 #,###

日期格式化是在SpringMVC启动时完成初始化的,我们不能干预,所以才提供这两个注解用来进行格式的定义。

比方说我们在客户端提供提交日期和金钱:

<form action="${pageContext.request.contextPath}/formatterTest" method="post">
   姓名:<input class="common-text required"  name="name" size="50" type="text" value="" >
   日期:<input class="common-text required" name="date" size="50" type="date">
   金额:<input class="common-text" name="money" size="50" value="" type="text">
   <input class="btn btn-primary btn6 mr10" value="提交" type="submit">
   <input class="btn btn6" οnclick="history.go(-1)" value="返回" type="button">
</form>

创建一个实体类:

public class FormatTest {
    private String name;
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private Date date;
    @NumberFormat(style = NumberFormat.Style.CURRENCY)
    private BigDecimal money;
    ......get、set..........
}

controller

@RequestMapping("/formatterTest")
public String formatterTest(FormatTest formatTest,Model model){
    model.addAttribute("format",formatTest);
    return "jsppage/formatTest";
}

JSP接受返回的数据

<form:form modelAttribute="format">
    日期:<form:input path="date"/>
        <br/>
    货币:<form:input path="money"/>
</form:form>
image-20220916162334374 image-20220916162352627
2、FormatterRegistry

使用用来注册格式化器的,但是它又继承了ConverterRegistry,所以也可以对类型转换器进行注册。

public interface FormatterRegistry extends ConverterRegistry {
    //添加打印机以打印特定类型的字段。字段类型由参数化的 Printer 实例隐含。
    void addPrinter(Printer<?> printer);
    //添加一个解析器来解析特定类型的字段。字段类型由参数化的 Parser 实例隐含
    void addParser(Parser<?> parser);
    //添加格式化程序以格式化特定类型的字段。字段类型由参数化的 Formatter 实例隐含。
    void addFormatter(Formatter<?> formatter);
    //添加格式化程序以格式化给定类型的字段。
    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
    //添加打印机/解析器对以格式化特定类型的字段。
    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
    //添加格式化程序以格式化用特定格式注释注释的字段。
    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

虽然格式化转换器和类型转换器是两个模块,但是Formatter 内部逻辑其实是委托给 Converter 去实现的,FormatterRegistry 有一个 FormattingConversionService 实现类,它是 ConversionService 接口的实现类,不仅具有类型转换功能,还具有格式化转换功能。它还有一个专门的工厂类 FormattingConversionServiceFactroyBean ,用来创造 FormattingConversionService 实例。

  • FormattingConversionService

  • DefaultFormattingConversionService:继承自FormattingConversionService,实际使用时,基本就是使用它,它默认配置了适用于大多数应用程序的转换器和格式化程序。

    public class DefaultFormattingConversionService extends FormattingConversionService {
    	/**
    	添加适用于大多数环境的格式化程序:包括数字格式化程序、JSR-354 Money & Currency 格式化程序、JSR-310 Date-Time 和/或 Joda-Time 格式化程序,具体取决于类路径上相应 API 的存在。
    	 */
    	@SuppressWarnings("deprecation")
    	public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
    		//数值的默认处理
    		formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
    
    		//货币值的默认处理
    		if (jsr354Present) {
    			formatterRegistry.addFormatter(new CurrencyUnitFormatter());
    			formatterRegistry.addFormatter(new MonetaryAmountFormatter());
    			formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
    		}
    		// 日期时间值的默认处理
    
    		// 只处理 JSR-310 特定的日期和时间类型
    		new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
    
    		if (jodaTimePresent) {
    			// 处理特定于 Joda 的类型以及 Date、Calendar、Longorg.springframework.format.datetime.joda.JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry);
    		}
    		else {
    			// 常规的基于 DateFormat 的日期、日历、长转换器
    			new DateFormatterRegistrar().registerFormatters(formatterRegistry);
    		}
    	}
    }
    

Spring支持的两个默认的转换器:DefaultConversionService(类型转换服务实现)、DefaultFormattingConversionService(带数据格式化支持的类型转换服务)。这两个都实现了ConverterRegistry、ConversionService接口。

而DefaultFormattingConversionService在功能上比 DefaultConversionService更加强大,所以若是Web环境下比如Spring MVC使用转换器、格式化器。建议使用 FormattingConversionServiceFactoryBean注册。(在SpringBoot中 WebMvcConfigurationSupport 默认使用的就是 FormattingConversionServiceFactoryBean)。

2.3.3、验证器

验证器是数据效验的最后一步,此时的数据已经进行完类型转换和格式化了,但是在真正使用前还需要验证数据的正确性。数据验证也分为两种,客户端验证和服务器端验证,客户端验证主要过滤正常用户的误操作,通过JavaScript代码实现。服务器端验证是整个应用阻止非法数据的最后防线,通过应用中编程实现,服务器端验证对于系统的安全性,完整性,健壮性起到至关重要的作用。

在Spring中验证数据的正确是一般是通过自带的验证器和JSR303两种方式实现。

1、JSR 303

JSR是Java Specification Requests的缩写,意思是Java 规范提案,它提供了对Bean的效验,通过一系列注解,能够对表单提交的每个数据都做一些简单的判定,

注解说明
@Null元素必须为 null
@NotNull元素必须不为 null
@AssertTrue元素必须为 true
@AssertFalse元素必须为 false
@Min(value)元素必须为数字且值必须大于等于指定的最小值
@Max(value)元素必须为数字且值必须小于等于指定的最小值
@DecimalMin(value)同@Min(value)
@DecimalMax(value)同@Max(value)
@Size(max, min)元素的大小必须在指定的范围内
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

使用JSR 303前先导入依赖

<!--    jsr303相关注解-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<!--    jar3032个运行依赖的相关包-->
<dependency>
    <groupId>com.fasterxml</groupId>
    <artifactId>classmate</artifactId>
    <version>1.5.1</version>
</dependency>
<dependency>
    <groupId>org.jboss.logging</groupId>
    <artifactId>jboss-logging</artifactId>
    <version>3.4.3.Final</version>
</dependency>
<!--    jsr303通过Hibernate规则效验-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.3.6.Final</version>
</dependency>

创建表单提交数据

<form action="${pageContext.request.contextPath}/jsr303Test" method="post">
    价格:<input class="common-text required"  name="price" size="50" type="number" value="" >
    数量:<input class="common-text required" name="count " size="50" type="number">
    总计:<input class="common-text" name="total" size="50" value="" type="number">
    备注:<input class="common-text" name="note" size="50" value="" type="text">
    <input class="btn btn-primary btn6 mr10" value="提交" type="submit">
    <input class="btn btn6" οnclick="history.go(-1)" value="返回" type="button">
</form>

创建实体类,使用注解加验证

public class Jsr303Test {
    @DecimalMin(value = "0.1")
    @NotNull
    private double price;

    @Min(1)
    @Max(10000)
    @NotNull
    private int count;

    @DecimalMin(value = "1.00")
    @DecimalMax(value = "10000000")
    @NotNull
    private double total;

    @Size(min = 0,max = 255)
    private String note;

    .............................
}

controller:打印错误信息

@RequestMapping("/jsr303Test")
public String jsr303Test(Jsr303Test jsr303,  Errors errors,Model model){
    if(errors.hasErrors()){
        List<FieldError> fieldErrors = errors.getFieldErrors();
        fieldErrors.forEach(f -> {
            System.out.println("fied:"+f.getField());
            System.out.println("message:"+f.getDefaultMessage());
        });
    }
    model.addAttribute("jsr303",jsr303);
    return "jsppage/jsr303View";
}

提交数据,全部设置为0,如果不符合规则,,打印错误信息

image-20220916181115007

image-20220916181138113

2、Spring验证器

JSR 303可以对参数进行一些约束和效验,但是有些业务效验JSR303 是不能实现的,这就需要使用Spring的验证器了。(难受的是Validator 验证器和JSR303的不能同时使用,同时使用只会有Validator 的验证)

Spring提供了一个 org.springframework.validation.Validator.Validator 接口来实现验证,它将在进入控制器逻辑之前对参数的合法性进行效验。Validator 是验证表单逻辑的核心接口

public interface Validator {
	//判断当前验证器是否用于检验clazz类型的pojo
    boolean supports(Class<?> clazz);

   //检验POJO的合法性
    void validate(Object target, Errors errors);
}

它在Spring加载时,会被注册到验证器列表中,然后提供给控制前使用定义,用supports方法判定是否会启用验证器验证数据,而validate 是验证的逻辑。

使用JSR303的例子,把JSR303相关代码去掉。我们创建自定义Spring验证器需要实现Validator接口

public class TestValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        //判断是否是Jsr303类型的
        return Jsr303.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Jsr303 jsr303 = (Jsr303) target;
        //规定:总计=价格*数量,才符合规则
        if(jsr303.getTotal() !=  (jsr303.getPrice() * jsr303.getCount())){
           errors.rejectValue("total","","不合法:总计=价格*数量");
        }
    }
}

必须把这个验证器捆绑到相应的Controller中才可以使用。

@InitBinder
public void initBinder(DataBinder dataBinder){
    dataBinder.setValidator(new TestValidator());
}
@RequestMapping("/jsr303Test")
public String jsr303Test(@Validated Jsr303 jsr303, Errors errors, Model model){
    if(errors.hasErrors()){
        List<FieldError> fieldErrors = errors.getFieldErrors();
        fieldErrors.forEach(f -> {
            System.out.println("fied:"+f.getField());
            System.out.println("message:"+f.getDefaultMessage());
        });
    }
    model.addAttribute("jsr303",jsr303);

    return "jsppage/jsr303View";
}
image-20220916185133702

image-20220916185153736

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

辰 羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值