springboot国际化和validation融合
场景
在应用交互时,可能需要根据客户端得语言来返回不同的语言数据。
前端通过参数、请求头等往后端传入locale相关得参数,后端获取参数,根据不同得locale来获取不同得语言得文本信息返回给前端。
实现原理
springboot支持国际化和validation,主要通过messagesource接口和validator实现。
国际化配置
- 编写国际化配置文件,如
messages_en_us.properties
和messages_zh_cn.properties
,并置于resources/i18n
目录下。 - 配置
application.yml
或application.properties
以指定国际化文件的位置,例如spring.messages.basename=i18n/messages
。 - 配置
localeresolver
以解析当前请求的locale,常用的实现是acceptheaderlocaleresolver
,它通过请求头accept-language
获取当前的locale。
validation配置
引入spring-boot-starter-validation
依赖以支持validation功能
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-validation</artifactid> </dependency>
配置localvalidatorfactorybean
以使用国际化的校验消息,需注入messagesource
示例
引入依赖
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-validation</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency>
国际化配置文件
在src/main/resources/i18n
目录下创建两个文件:messages_en_us.properties
和messages_zh_cn.properties
。
#messages_en_us.properties welcome.message=welcome to our website! #messages_zh_cn.properties welcome.message=欢迎来到我们的网站!
配置messagesource
在spring boot的配置文件中(application.properties
或application.yml
),配置messagesource
以指定国际化文件的位置。
如果你打算使用validation的默认国际化文件,你实际上不需要为validation单独指定文件,因为localvalidatorfactorybean
会自动查找validationmessages.properties
。
但是,你可以配置自己的国际化文件,并让messagesource
同时服务于你的应用消息和validation消息。
# 国际化文件被放置在src/main/resources/i18n目录下,并以messages为前缀 spring.messages.basename=i18n/messages,org.hibernate.validator.validationmessages spring.messages.encoding=utf-8
注意:上面的配置假设你的自定义消息文件位于i18n/messages.properties
,而validation的默认消息文件是org.hibernate.validator.validationmessages.properties
。
实际上,validationmessages.properties
文件位于hibernate validator的jar包中,所以你不需要显式地将它包含在你的资源目录中。spring boot会自动从classpath中加载它。
配置localvalidatorfactorybean
在你的配置类中,创建一个localvalidatorfactorybean
的bean,并将messagesource
注入到它中。
这样,localvalidatorfactorybean
就会使用spring的messagesource
来解析校验消息。
import org.hibernate.validator.hibernatevalidator; import org.springframework.context.messagesource; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.validation.beanvalidation.localvalidatorfactorybean; import java.util.properties; @configuration public class validateconfig { @bean public localvalidatorfactorybean validatorfactorybean(messagesource messagesource) { localvalidatorfactorybean factorybean = new localvalidatorfactorybean(); factorybean.setvalidationmessagesource(messagesource); // 设置使用 hibernatevalidator 校验器 factorybean.setproviderclass(hibernatevalidator.class); // 设置 快速异常返回 只要有一个校验错误就立即返回失败,其他参数不在校验 properties properties = new properties(); properties.setproperty("hibernate.validator.fail_fast", "true"); factorybean.setvalidationproperties(properties); // 加载配置 factorybean.afterpropertiesset(); return factorybean; } }
使用校验
import javax.validation.constraints.notnull; public class mymodel { @notnull(message = "{not.null.message}") private string field; // getters and setters }
messages.properties
文件中,你可以添加
not.null.message=this field cannot be null.
而在hibernate validator的validationmessages.properties
文件中,已经包含了默认的校验消息,如{javax.validation.constraints.notnull.message}
的值。
自定义校验
- 定义约束注解:创建一个注解,用
@constraint
标记,并定义message
、groups
和payload
属性
import javax.validation.constraint; import javax.validation.payload; import java.lang.annotation.*; @documented @target({elementtype.parameter, elementtype.field, elementtype.type}) @retention(retentionpolicy.runtime) @constraint(validatedby = myvalidatecontent.class) public @interface myvalidate { string message() default ""; class<?>[] groups() default {}; class<? extends payload>[] payload() default {}; }
- 实现约束验证器**:创建一个实现了
constraintvalidator
接口的类,并重写isvalid
方法 - 在
isvalid
方法中使用constraintvalidatorcontext
**:如果验证失败,使用constraintvalidatorcontext
的buildconstraintviolationwithtemplate
方法来构建constraintviolation
import com.example.dto.paramvo; import javax.validation.constraintvalidator; import javax.validation.constraintvalidatorcontext; public class myvalidatecontent implements constraintvalidator<myvalidate, paramvo> { @override public void initialize(myconstraint constraintannotation) { // 初始化代码(如果需要的话) } @override public boolean isvalid(paramvo paramvo, constraintvalidatorcontext constraintvalidatorcontext) { if ("n".equals(paramvo.getsex())) { if (paramvo.getage() < 18) { buildmessage(constraintvalidatorcontext, "template1"); return false; } } else { if (paramvo.getage() < 20) { buildmessage(constraintvalidatorcontext, "template2"); return false; } } return true; } private void buildmessage(constraintvalidatorcontext context, string key) { string template = ('{'+key+'}').intern(); context.buildconstraintviolationwithtemplate(template) .addconstraintviolation(); } }
在这个例子中,如果sex
是n
并且age
小于18
,验证器将使用constraintvalidatorcontext
来构建一个带有错误消息的constraintviolation
。
消息模板"{template1}"
将会在验证失败时被解析,并替换为你在myvalidate
注解中定义的默认消息或你在messages.properties
文件中定义的国际化消息。
确保你的myvalidate
注解定义了一个message
属性,并且你在messages.properties
文件中有一个对应的条目例如:
template1=男性要大于18 template2=女性要大于20
import com.example.validate.myvalidate; import lombok.getter; import lombok.setter; import org.hibernate.validator.constraints.length; import javax.validation.constraints.notblank; import javax.validation.constraints.notnull; @getter @setter @myvalidate public class paramvo { @notblank(message = "{javax.validation.constraints.notnull.message}") private string sex; @notnull(message = "age 不能为空") private integer age; @notblank(message = "{name.not.null}") @length(max = 3,message = "{name.length.max}") private string name; }
controller层异常处理
import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.validation.bindingresult; import org.springframework.validation.fielderror; import org.springframework.web.bind.methodargumentnotvalidexception; import org.springframework.web.bind.annotation.exceptionhandler; import org.springframework.web.bind.annotation.restcontrolleradvice; import java.util.hashmap; import java.util.map; @restcontrolleradvice public class globalexception { @exceptionhandler(methodargumentnotvalidexception.class) public responseentity<object> handlevalidationexceptions(methodargumentnotvalidexception ex) { map<string, string> errors = new hashmap<>(); bindingresult result = ex.getbindingresult(); for (fielderror error : result.getfielderrors()) { errors.put(error.getfield(), error.getdefaultmessage()); } // 这里可以根据实际需求定制返回的错误信息结构 map<string, object> response = new hashmap<>(); response.put("status", httpstatus.bad_request.value()); response.put("errors", errors); return new responseentity<>(response, httpstatus.bad_request); } }
内部方法校验
import org.springframework.validation.annotation.validated; import javax.validation.constraints.min; import javax.validation.constraints.notnull; //@validated @validated public interface serviceintface { //校验返回值,校验入参 @notnull object hello(@notnull @min(10) integer id, @notnull string name); }
import lombok.extern.slf4j.slf4j; import org.springframework.stereotype.service; @slf4j @service public class serviceimpl implements serviceintface { @override public object hello(integer id, string name) { return null; } }
import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.web.bind.annotation.exceptionhandler; import org.springframework.web.bind.annotation.restcontrolleradvice; import javax.validation.constraintviolation; import javax.validation.constraintviolationexception; import java.util.hashmap; import java.util.map; import java.util.set; @restcontrolleradvice public class globalexception { @exceptionhandler(constraintviolationexception.class) public responseentity<object> handlevalidationexceptions(constraintviolationexception ex) { map<string, string> errors = new hashmap<>(); set<constraintviolation<?>> constraintviolations = ex.getconstraintviolations(); for (constraintviolation<?> constraintviolation : constraintviolations) { string key = constraintviolation.getpropertypath().tostring(); string message = constraintviolation.getmessage(); errors.put(key, message); } // 这里可以根据实际需求定制返回的错误信息结构 map<string, object> response = new hashmap<>(); response.put("status", httpstatus.bad_request.value()); response.put("errors", errors); return new responseentity<>(response, httpstatus.bad_request); } }
- 校验写在接口上的,抛出异常
javax.validation.constraintviolationexception
- 校验写在具体实现,抛出异常
javax.validation.constraintdeclarationexception
注意点
代码中国际化使用
代码里响应,手动获取使用messagesource的getmessage方法即可,也就是spring容器中的getmessage()
# messages_en_us.properties welcome.message=welcome to our website! # messages_zh_cn.properties welcome.message=欢迎来到我们的网站! #定义消息,并使用占位符{0}、{1}等表示参数位置 #welcome.message=欢迎{0}来到{1}
//创建一个配置类来配置localeresolver,以便根据请求解析当前的语言环境: import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.localeresolver; import org.springframework.web.servlet.config.annotation.webmvcconfigurer; import org.springframework.web.servlet.i18n.sessionlocaleresolver; import java.util.locale; @configuration public class webconfig implements webmvcconfigurer { @bean public localeresolver localeresolver() { sessionlocaleresolver sessionlocaleresolver = new sessionlocaleresolver(); sessionlocaleresolver.setdefaultlocale(locale.us); // 设置默认语言 return sessionlocaleresolver; } }
//创建一个控制器来使用国际化的消息 import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.messagesource; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.restcontroller; import javax.servlet.http.httpservletrequest; import java.util.locale; @restcontroller @requestmapping("/hello") public class hellocontroller { @autowired private messagesource messagesource; @getmapping public string hello(httpservletrequest request) { locale locale = (locale) request.getattribute(org.springframework.web.servlet.localeresolver.locale_resolver_attribute); //messagesource.getmessage("welcome.message", new object[]{"张三", "中国"}, locale.china)。 return messagesource.getmessage("welcome.message", null, locale); } }
locale获取
默认情况下spring注册的messagesource对象为resourcebundlemessagesource,会读取spring.message
配置。
请求中locale的获取是通过localeresolver
进行处理,默认是acceptheaderlocaleresolver
,通过webmvcautoconfiguration
注入,从accept-language
请求头中获取locale信息。
此时前端可以在不同语言环境时传入不同的请求头accept-language即可达到切换语言的效果
accept-language: en-us accept-language: zh-cn
默认情况下前端请求中的不用处理,如果约定其他信息传递local,使用自定义的i18nlocaleresolver替换默认的acceptheaderlocaleresolver
,重写resolvelocale
方法就可以自定义locale的解析逻辑。
import cn.hutool.core.util.strutil; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.localeresolver; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import java.util.locale; /** * */ @configuration public class i18nconfig { @bean public localeresolver localeresolver() { return new i18nlocaleresolver(); } /** * 获取请求头国际化信息 * 使用自定义的i18nlocaleresolver替换默认的acceptheaderlocaleresolver,重写resolvelocale方法就可以自定义locale的解析逻辑。 * * 自定义后使用content-language传locale信息,使用_划分语言个地区。 * content-language: en_us * content-language: zh_cn */ static class i18nlocaleresolver implements localeresolver { @override public locale resolvelocale(httpservletrequest httpservletrequest) { string language = httpservletrequest.getheader("content-language"); locale locale = locale.getdefault(); if (strutil.isnotblank(language)) { string[] split = language.split("_"); locale = new locale(split[0], split[1]); } return locale; } @override public void setlocale(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse, locale locale) { } } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论