使用过滤器实现后台返回Response国际化
前言 :
写这篇文档之前,其实我看过了spring的国际化处理,使用spring去处理国际化也确实方便,但由于公司项目是已经做好了的,只有一个中文版,如果直接改成用spring的话,需要改的代码量非常大,所以我就想着根据自己的项目,然后模仿spring的做法去实现国际化。
一、前端打算怎么改
现在我们做的项目是前后端分离的,也就是后端的请求全部返回JSON数据,前端全部使用Ajax去处理后台返回的数据,所以前端页面的国际化实现,实际上就是copy一份中文版的项目,然后把页面上的中文全部改成英文的,这种做法实在是最蠢的,如果以后需要添加更多的语言支持,岂不是要copy好几份出来,所以这种做法是不可取的,虽然我们是用的这种做法。更好的方法是前端也使用国际化框架,这里比较常用的就是jquery.i18n.properties的使用讲解与实例这个了,有兴趣的可以看看,反正本文主要讲的是后台的实现方法。
二、后台的实现
1、思路
我们模仿spring的做法,定义两套properties文件,一个中文一个英文,然后通过前端的页面路径去判断是中文版的前端访问还是英文版的前端在访问,这里通过路劲去判断只是针对我现在做的这个项目,上面说了前端的页面会改成两套,所以到时候资源路径上中文那套上面带有
pc-cn,英文版的自然就是
pc-en了,我就通过这个判断了
,如果前端使用了国际化框架的话,我们就可以去从请求头获取pc-cn
、pc-en
了,前端就需要在请求头里面添加pc-cn
、pc-en
这两个了,后台使用一个过滤器过滤response,实际上就是动态去加载properties中的value去替换response中的返回值。
2、返回值
后台的返回值是用自定义的一个对象封装起来的,代码如下:
package com.tradeplatform.common.vo;
import java.util.HashMap;
/**
* 返回结果map。继承于HashMap,作为controller方法返回值,会被自动转换为json发送到前端,键值对就是json的键值对。
* <br><br>
* 创建人:yuanwl <br>
* 创建时间:2018年4月14日 下午2:34:30 <br>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
*
* @version V1.0
*/
public class ResultMap extends HashMap<String, Object> {
private static final long serialVersionUID = -4970973511892646114L;
/** 返回结果(数据)键 */
public final static String KEY_DATA = "data";
/** 返回成功判断码键 */
public final static String SUCCESS_CODE = "success";
/** 返回结果(数据)键 */
public final static String KEY_RESULT = "result";
/** 返回错误结果键 */
public final static String KEY_ERROR = "error";
/** 返回状态码键 */
public final static String KEY_STATUS_CODE = "statusCode";
/** 返回结果描述键 */
public final static String KEY_RESULT_DESC = "resultDesc";
/** 默认返回成功状态码。所有成功結果都用0表示 */
public static final int VALUE_DEAFULT_SUCCESSFUL_STATUS_CODE = 0;
/** 默认返回错误状态码。大部分的錯誤結果都用1表示,还可以自定义错误结果类型 */
public static final int VALUE_DEAFULT_ERROR_STATUS_CODE = 1;
public ResultMap() {
this(VALUE_DEAFULT_SUCCESSFUL_STATUS_CODE, null, null);
this.put(KEY_ERROR, false);
}
public ResultMap(Object result) {
this.put(KEY_ERROR, false);
this.put(KEY_RESULT, result);
}
public ResultMap(int statusCode, Object result, String desc) {
this.put(KEY_STATUS_CODE, statusCode);
this.put(KEY_RESULT, result);
this.put(KEY_RESULT_DESC, desc);
}
public ResultMap(String success, Object result) {
this.put(SUCCESS_CODE, true);
this.put(KEY_DATA, result);
}
/**新K线图数据返回格式*/
public static ResultMap getSuccessfulDate(Object result, String desc) {
return new ResultMap(SUCCESS_CODE, result);
}
/**新K线图数据返回格式*/
public static ResultMap getSuccessfulDate(Object result) {
return getSuccessfulDate(result, null);
}
public static ResultMap getSuccessfulResult(Object result, String desc) {
return new ResultMap(VALUE_DEAFULT_SUCCESSFUL_STATUS_CODE, result, desc);
}
public static ResultMap getSuccessfulResult(Object result) {
return getSuccessfulResult(result, null);
}
public static ResultMap getSuccessfulResult(String desc) {
return getSuccessfulResult(null, desc);
}
public static ResultMap getSuccessfulResult() {
return getSuccessfulResult(null, null);
}
public static ResultMap getFailureResult(int statusCode, Object result,
String desc) {
return new ResultMap(statusCode, result, desc);
}
public static ResultMap getFailureResult(int statusCode, Object result) {
return getFailureResult(statusCode, result, null);
}
public static ResultMap getFailureResult(int statusCode, String desc) {
return getFailureResult(statusCode, null, desc);
}
public static ResultMap getFailureResult(int statusCode) {
return getFailureResult(statusCode, null, null);
}
public static ResultMap getFailureResult(Object result, String desc) {
return getFailureResult(VALUE_DEAFULT_ERROR_STATUS_CODE, result, desc);
}
public static ResultMap getFailureResult(Object result) {
return getFailureResult(result, null);
}
public static ResultMap getFailureResult(String desc) {
return getFailureResult(null, desc);
}
public static ResultMap getFailureResult() {
return getFailureResult(null, null);
}
public int getStatusCode(){
Object status = get(KEY_STATUS_CODE);
return status == null? VALUE_DEAFULT_ERROR_STATUS_CODE:VALUE_DEAFULT_SUCCESSFUL_STATUS_CODE;
}
}
后台调用的时候是这样的:
最后前端显示:
从上面的代码可以看出,后台返回给前端的提示是放在resultDesc
里面的,所以我们只需要动态去替换resultDesc
中的提示。
3、properties文件
定义两套properties文件,用来存放国际化的字符串,zh_CN.properties
[中文]和en_US.properties
[英文],像下面这样:
这里需要注意的是,俩个properties文件的key要保持一致,以确保能正常取值。
4、过滤器
过滤器不会用的看这里:Java过滤器Filter使用详解
过滤器直接看代码,注释很详细
package com.tradeplatform.common.servlet;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tradeplatform.common.util.ApiConstant;
import com.tradeplatform.common.util.SysConfig;
import com.tradeplatform.common.vo.ResultMap;
/**
* 实现国际化过滤器
* @author asus
*/
public class I18NFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String ZH_CN = "cn";
private static final String EN_US = "en";
// private static final String ZH_CN = "/zh-cn";
// private static final String EN_US = "/en-us";
/**
* 测试环境协议
*/
private static final String PROTOCOL_TEST = "http://";
/**
* 正式环境协议
*/
private static final String PROTOCOL_PROD = "https://";
/**
* 主机名
*/
private static String host = null;
/**
* 获取资源路径
*/
private static String referer = null;
/**
* 获取请求头中的语言参数
*/
private static String language = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletResponse resp = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest)request;
/**
* 获取请求头中的语言参数
*/
language = req.getHeader("Language");
//获取请求的URI
String uri = req.getRequestURI();
// logger.info(">>>>>>>>>>国际化过滤器获取请求的URI" + uri + "<<<<<<<<<");
if (!uri.contains("platform/user/getValidateCodeImg") && !uri.contains("platform/user/getQRCode")) {
// logger.info(">>>>>>>>>>请求进入国际化过滤器<<<<<<<<<");
// logger.info("language------>" + language);
//请求头中language不为空,则为Android/IOS访问
if (StringUtils.isNotBlank(language)) {
logger.info(">>>>>> Android/IOS访问 <<<<<<");
handleResponse(request, response, resp, chain);
}else {
// 获取资源路径
referer = req.getHeader("referer");
// logger.info(">>>>>>>>>>国际化过滤器获取资源路径" + referer + "<<<<<<<<<");
/**
* 判断资源路径是否为空,uri是否为图片验证码的和二维码的,图片验证码和二维码是返回的流数据
*/
if (StringUtils.isNotBlank(referer) && !referer.contains("admin")) {
//浏览器访问
logger.info(">>>>>> 浏览器访问 <<<<<<");
try {
URL url = new URL(referer);
host = url.getHost();// 获取主机名
// logger.info(">>>>>>>>>>国际化过滤器获取主机名" + host + "<<<<<<<<<");
} catch (MalformedURLException e1) {
logger.error("获取主机名异常", e1);
}
handleResponse(request, response, resp, chain);
}else {
// logger.info(">>>>>> 国际化过滤器不做处理 <<<<<<");
try {
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
} catch (Exception e) {
logger.info("处理国际化返回结果失败", e);
}
}
}
}else {
// logger.info(">>>>>> 国际化过滤器不做处理 <<<<<<");
try {
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
} catch (Exception e) {
logger.info("处理国际化返回结果失败", e);
}
}
}
private void handleResponse(ServletRequest request, ServletResponse response, HttpServletResponse resp, FilterChain chain) {
// 包装响应对象 resp 并缓存响应数据
WrapperResponse mResp = new WrapperResponse(resp);
ServletOutputStream out = null;
try {
out = response.getOutputStream();
//防止出现乱码
mResp.setCharacterEncoding("UTF-8");
chain.doFilter(request, mResp);
// 获取缓存的响应数据
byte[] bytes = mResp.getBytes();
String str = new String(bytes, "UTF-8");
//判断响应数据是否包含result_desc
if (str.contains(ResultMap.KEY_RESULT_DESC)) {
//将String类型响应数据转成Map,方便取出result_desc
@SuppressWarnings("unchecked")
Map<String,String> m = new ObjectMapper().readValue(str, Map.class);
String key = m.get(ResultMap.KEY_RESULT_DESC);
//这里因为result_desc可能为空
if (StringUtils.isNotBlank(key)) {
//用户保存最后的Value
StringBuilder val = new StringBuilder();
/**
* 根据key获取resultDesc的value
* 因为可能resultDesc中的数据可能是分开的,像这样
* return ResultMap.getFailureResult(PlatformResultConstants.STATUS_ERRORCODE_USER_NOT_FIND,
"推荐人:&" + username + "&的GIRL钱包余额未曾大于零,不能推荐!");
*/
if (key.contains("&")) {
String[] keys = key.split("\\&");
for (String s : keys) {
val.append(getVal(s));
}
} else {
val.append(getVal(key));
}
str = str.replace(key, val);
}
bytes = str.getBytes();
}
out.write(bytes);
} catch (Exception e) {
logger.error("处理国际化返回结果失败", e);
} finally {
try {
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 根据properties文件中属性的key获取对应的值
* 说明:
* <p>
* 创建人: LGQ <br>
* 创建时间: 2018年8月13日 下午8:10:20 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
*/
private String getVal(String key) {
String val = null;
if (StringUtils.isNotBlank(language)) {
if (language.equals(ZH_CN)) {
val = ResultDesc.getCnValue(key);
}else if (language.equals(EN_US)) {
val = ResultDesc.getEnValue(key);
}else {
val = ResultDesc.getCnValue(key);
}
}else {
String PROTOCOL = SysConfig.environment >= ApiConstant.Environment.OBT ? PROTOCOL_PROD : PROTOCOL_TEST;
// logger.info(PROTOCOL + host + ZH_CN);
// logger.info(referer);
/**
* 判断资源是以什么开头的
*/
if (StringUtils.isNotBlank(referer) && referer.startsWith(PROTOCOL + host + "/" + ZH_CN)) {
val = ResultDesc.getCnValue(key);
}else if (StringUtils.isNotBlank(referer) && referer.startsWith(PROTOCOL + host + "/" + EN_US)) {
val = ResultDesc.getEnValue(key);
}else {
//默认返回中文
val = ResultDesc.getCnValue(key);
}
}
if (StringUtils.isBlank(val)) {
val = key;
}
logger.info(key + "================" + val);
return val;
}
@Override
public void destroy() {}
}
别忘记在web.xml中配置过滤器:
<filter>
<filter-name>i18NFilter</filter-name>
<filter-class>com.tradeplatform.common.servlet.I18NFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>i18NFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
当然也可以使用注解:
/**
* 实现国际化过滤器
*/
@WebFilter(filterName="过滤器名称",urlPatterns="/*")
public class I18NFilter implements Filter {
......
}
WrapperResponse.java
package com.tradeplatform.common.servlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class WrapperResponse extends HttpServletResponseWrapper{
private ByteArrayOutputStream bytes = new ByteArrayOutputStream();
private HttpServletResponse response;
private PrintWriter pwrite;
public WrapperResponse(HttpServletResponse response) {
super(response);
this.response = response;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new MyServletOutputStream(bytes); // 将数据写到 byte 中
}
/**
* 重写父类的 getWriter() 方法,将响应数据缓存在 PrintWriter 中
*/
@Override
public PrintWriter getWriter() throws IOException {
try{
pwrite = new PrintWriter(new OutputStreamWriter(bytes, "utf-8"));
} catch(UnsupportedEncodingException e) {
e.printStackTrace();
}
return pwrite;
}
/**
* 获取缓存在 PrintWriter 中的响应数据
* @return
*/
public byte[] getBytes() {
if(null != pwrite) {
pwrite.close();
return bytes.toByteArray();
}
if(null != bytes) {
try {
bytes.flush();
} catch(IOException e) {
e.printStackTrace();
}
}
return bytes.toByteArray();
}
class MyServletOutputStream extends ServletOutputStream {
private ByteArrayOutputStream ostream ;
public MyServletOutputStream(ByteArrayOutputStream ostream) {
this.ostream = ostream;
}
@Override
public void write(int b) throws IOException {
ostream.write(b); // 将数据写到 stream 中
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
// TODO Auto-generated method stub
}
}
}
ResultDesc.java
package com.tradeplatform.common.servlet;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.support.PropertiesLoaderUtils;
/**
* 用于控制国际化返回值
* @author asus
*
*/
public class ResultDesc {
private static Logger logger = LoggerFactory.getLogger(ResultDesc.class);
private static Properties cnProps;
private static Properties enProps;
private static final String ZH_CN_PROPERTIES_NAME = "zh_CN.properties";
private static final String EN_US_PROPERTIES_NAME = "en_US.properties";
/**
* 获取zh_CN.properties的值
* </p>
*/
public static String getCnValue(String key) {
key = key.trim();
/**
* 读取中文properties文件
*/
if (null == cnProps) {
try {
logger.info("开始初始化配置文件" + ZH_CN_PROPERTIES_NAME);
cnProps = PropertiesLoaderUtils.loadAllProperties(ZH_CN_PROPERTIES_NAME);
} catch (IOException e) {
logger.error("读取配置文件" + ZH_CN_PROPERTIES_NAME + "失败", e);
}
}
//根据name得到对应的value
String val = cnProps.getProperty(key);
try {
if (StringUtils.isNotBlank(val)) {
//为了防止出现乱码
val = new String(val.getBytes("ISO-8859-1"),"UTF-8");
}
} catch (UnsupportedEncodingException e) {
logger.error("读取配置文件" + ZH_CN_PROPERTIES_NAME + "中的值时转码失败", e);
}
return val;
}
/**
* 获取en_US.properties的值
* </p>
*/
public static String getEnValue(String key) {
key = key.trim();
/**
* 读取英文properties文件
*/
if (null == enProps) {
try {
logger.info("开始初始化配置文件" + EN_US_PROPERTIES_NAME);
enProps = PropertiesLoaderUtils.loadAllProperties(EN_US_PROPERTIES_NAME);
} catch (IOException e) {
logger.error("读取配置文件" + EN_US_PROPERTIES_NAME + "失败", e);
}
}
return enProps.getProperty(key);
}
}
上面获取properties文件使用的spring的:
5、使用
我们只需要把返回结果中的resultDesc替换为properties文件中的key就可以了,如果有多个key就用&
分开。
最后感谢: