前言
Hi,各位掘金的掘友们好久不见呀!今天小甲将会为大家分享独立站多站点币种切换这个非常常见的功能需求,相信不少小伙伴都有逛过诸如虾皮,亚马逊等海外电商站点,那么对这些网站的币种切换功能肯定是见怪不怪,各位程序员小伙伴对这样的功能一般都会有啥思考做法呢?接下来,就让小甲带领各位开启多货币分享之旅;
功能实现
技术栈:SpringBoot,MySQL,MyBatisPlus;
实现思路:
1.后端硬编码处理;对每一个需要进行处理的接口都进行币种转换,这种方式是对系统影响最低,但也是技术含量最低,最繁琐的处理方式,而且对后续业务拓展,版本的迭代化处理非常的不友好,一般该方案都不会被考虑;
2.前端硬编码处理;该方式同后端处理一样均会存在不好拓展迭代的问题,而且前端处理还会出现精度缺失等一系列数据计算问题;
3.接口响应体全局拦截方式,通过实现RestControllerAdvice的方式可以实现对响应体数据的全局拦截,示例代码如下:
@Slf4j
@ControllerAdvice
public class CurrencyResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return returnType.hasParameterAnnotation(ResponseBody.class) || returnType.getDeclaringClass().isAnnotationPresent(RestController.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body == null) {
return null;
}
Class> clazz = body.getClass();
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropezFyWyBmrtyDescriptors(clazz);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String name = propertyDescriptor.getName();
if("class".equals(name)){
continue;
}
Field field = ReflectionUtils.findField(clazz, name);
Class> fieldClazz = field.getType();
if(!fieldClazz.equals(BigDecimal.class) ){
continue;
}
if(!field.isAnnotationPresent(FenToYuan.class)) {
continue;
}
Method readMethod = propertyDescriptor.getReadMethod();
Method writeMethod = propertyDescriptor.getWriteMethod();
try {
BigDecimal fenAmount = (BigDecimal) readMethod.invoke(body);
BigDecimal yuanAmount = AmountUtils.fen2yuan(fenAmount);
writeMethod.invoke(body, yuanAmount);
} catch (Exception e) {
log.error("amount convert fen to yuan fail", e);
}
}
return body;
}
}
}
该种方式可对响应体的Response数据进行修改处理,但是由于我们响应体数据的不确定性而且该方式对于递归深层嵌套数据的支持也比较弱,递归对性能的影响也比较大,并且该处理方式会影响服务全局的数据响应,对于系统的侵入较大,风险不可把控,因此该方式也不可取;
4.SpringAop切面注解方式处理;该方式通过自定义注解,对需要进行处理的方面进行切面拦截处理,同方式一一样都是比较繁琐且侵入性较高,因此此种方式也是不可取;
讲到这里,相信大部分小伙伴看到这里都觉得小甲快把平常能用的做法都列完了。接下来,就让小甲和大家伙分享一种小甲认为比较能快速兼容并且风险点比较小的方式,所谓后端,都是crud Boy,那么,我们响应体需要转化币种的数据,自然也都是从数据存储源查询出的,因此,我们可以从ORM查询出来的数据进行着手处理(注:该方式仅适用于金币方式通过BigDecimal的数据类型,如果通过字符串进行金币的存储,请关注小甲的下一篇文章)
5.前端请求头+MyBatisPlus的BaseTypeHandler进行处理,由于币种之间的转换是存在着汇率的,汇率会提前拉取三方汇率转化并下发给前端,前端请求接口时将国家代码以及汇率转化的比例一并携带过来,后端ORM通过改写MyBatisPlus的BaseTypeHandler对BigDecimal进行拦截处理,从而达到全局拦截处理的效果,而且该方式需要携带请求头以及数据类型为指定类型才会进行处理,可控性和拓展性较大,对系统的影响较小,示例代码如下:
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
@MappedTypes(BigDecimal.class)
public class BigDecimalTypeHandler extends org.apache.ibatis.type.BigDecimalTypeHandler {
public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType) throws SQLException {
String rateNum = getRateNum();
if (StringUtils.isNotBlank(rateNum)) {
BigDecimal rate = new BigDecimal(rateNum);
ps.setBigDecimal(i,parameter.divide(rate,2,BigDecimal.ROUND_HALF_UP));
} else {
ps.setBigDecimal(i, parameter);
}
}
public BigDecimal getNullableResult(ResultSet rs, String columnName) throws SQLException {
String rateNum = getRateNum();
if (rs.getBigDecimal(columnName) != null && StringUtils.isNotBlank(rateNum)) {
BigDecimal rsBigDecimal = rs.getBigDecimal(columnName);
return rsBigDecimal.multiply(new BigDecimal(rateNum)).setScale(2, BigDecimal.ROUND_UP);
} else {
return rs.getBigDecimal(columnName);
}
}
private String getRateNum() {
if (Objects.nonNull(RequestContextHolder.getRequestAttributes())) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String rateNum = request.getHeader("rateNum");
return rateNum;
}
return null;
}
public BigDecimal getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String rateNum = getRateNum();
if (rs.getBigDecimal(columnIndex) != null && StringUtils.isNotBlank(rateNum)) {
BigDecimal rsBigDecimal = rs.getBigDecimal(columnIndex);
return rsBigDecimal.multiply(new BigDecimal(rateNum));
} else {
return rs.getBigDecimal(columnIndex);
}
}
public BigDecimal getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String rateNum = getRateNum();
if (cs.getBigDecimal(columnIndex) != null && StringUtils.isNotBlank(rateNum)) {
BigDecimal rsBigDecimal = cs.getBigDecimal(columnIndex);
return rsBigDecimal.multiply(new BigDecimal(rateNum));
} else {
return cs.getBigDecimal(columnIndex);
}
}
}
功能总结
通过对该需求做法的不断思考分析,最终小甲选择了比较符合先阶段功能迭代开发的做法对多货币的功能进行了快速的迭代开发,避免了采取硬编码的方式从而导致的后续开发拓展困难的情况,在性能与系统影响方面小甲考虑了对业务侵入较低,影响范围较小的数据源ORM拦截的方式来实现该业务功能的迭代开发,希望在日后的业务需求中,小甲能考虑到更多的方面,在不断的成长进步中,书写出又优雅又有性能的好代码出来;
碎碎念时光
首先很感谢能看完全篇幅的各位老铁兄弟们,希望本篇文章能对各位和秃头小甲一样码农有所帮助,当然如果各位技术大大对这模块做法有更优质的做法的,也欢迎各位技术大大能在评论区留言探讨,写在最后~~~~~~ 创作不易,希望各位老铁能不吝惜于自己的手指,帮秃头点下您宝贵的赞把!