自定义类中属性注解
- 前言
- 基础业务场景描述
- 功能实现
- 开始
- 一、自定义注解:@DoubleFormat
- 二、自定义json序列化解析
- 三、Controller测试
- 业务扩展
- 一、扩展业务场景描述
- 二、改造注解
- 三、改造解析器
- createContextual 方法解读
- 改造之后的代码
- 四、Controller测试
- 总结
- 源码
- 参考
前言
java自定义注解的出现,极大程度上解决的代码中重复造轮子的尴尬境地,一方面精简了代码,另一方面同时也提高了业务理解和代码可读性,本系列文章根据实际业务出发,大体讲解项目中常见的几种注解实现与解析,以供读者参考。
Tip:本文代码部分应用JAVA8新特性,如有纯萌新,请复习后观看
基础业务场景描述
相信大家日常项目中都会遇到如下情景:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsVO {
private Long id;
private String goodsName;
/**单价*/
private Double price;
}
这是一个商品VO类,业务要求返回的字段中所有 Double 类返回值为:取小数点后两位(四舍五入)后以 String形式传给前端。
之前重复造轮子的方法是在类中写入get方法来解决,如下:
public String getPrice() {
return Optional.ofNullable(this.price)
.map(item -> new DecimalFormat("#0.00").format(item))
.orElse("0.00");
}
这样就会出现,只要有Double 格式化的地方都要加上这个 get方法或者该方法引用,项目中会存在大量重复的“垃圾代码”,而且将来一旦业务修改,是不是每一个方法都要去修改?大大提高了维护成本。
所以
我们就想,如果可以写一个注解,打在我们需要转变的字段上,而注解解析方法只有一个(主体业务逻辑)
这样既利于我们的维护,也利会大大提高代码的简洁度和可读性。
功能实现
开始
注解往往是伴随着其解析器(解析方法),同时出现的,没有解析的注解是不会生效的,这里可以理解为你想要的核心逻辑代码,例如这里的 new DecimalFormat("#0.00") 也就是:取小数点后两位(四舍五入)后以 String形式传给前端,这个业务逻辑就是你的解析器,而我们定义的注解,往往起到的作用就是触发或者是指向解析器。
Tip: java 如何自定义创建一个自定义注解,以及其中注解生效方式及范围等,基本属于java初级基本功范围之内,这里就不再赘述。 如有问题请问百度
一、自定义注解:@DoubleFormat
基于上述场业务场景描述,我们打算自定义一个注解,这里我起名叫做 @DoubleFormat(见其名知其意)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
// 为该注解指定一个自定义的json序列化解析器
@JsonSerialize(using = DoubleFormatSerialize.class)
public @interface DoubleFormat {
}
其中@JsonSerialize(using = DoubleFormatSerialize.class)
是为该注解指定一个自定义的json序列化解析器稍后会讲到。
我们将创建好的注解打在字段上
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsVO {
private Long id;
private String goodsName;
/**商品名*/
@DoubleFormat
private Double price;
}
此时我们的注解不会生效,因为我们没有为他指明如要指向那些业务逻辑代码,这时就要用到我们自定义的解析器。
二、自定义json序列化解析
如上所述,我们希望当Double类型的字段打上 @DoubleFormat 注解之后,我们传到前端的json数据中,改字段应该格式化为:取小数点后两位(四舍五入)后的String形式。
此时我们需要自定义一个json序列化解析:
只需类继承 JsonSerializer<泛型> ,并Override其中的serialize方法即可
这里我们对Double进行处理则为:
extends JsonSerializer<Double>
我们新建一个类,取名为 DoubleFormatSerialize (尽量起名与注解相关,方便理解与管理)
代码如下:
public class DoubleFormatSerialize extends JsonSerializer<Double> {
@Override
public void serialize(Double price, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 小数点后保留位数 (后两位)
String pattern = "#0.00";
// 返回值
jsonGenerator.writeString(new DecimalFormat(pattern).format(price));
}
}
项目中文件为:
三、Controller测试
我们创建一个Controller进行测试看看是否达到我们的预期:
测试数据为Double型,数据为 38.2345
我们预期传到前端的json应为 “38.23” (取小数点后两位(四舍五入)后的String形式)
开始测试:我们先把注解去掉,看看原数据
数据为没有经过处理的原数据原数据,这时我们加上注解再看效果
这时json数据已经序列化为我们需要的数据,证明我们的自定义注解功能实现了
业务扩展
一、扩展业务场景描述
如上所述,我们已经初步完成自定义注解的功能设计,但是还是不能满足复杂可变的业务场景。
现有业务场景:要求原业务
取小数点后两位(四舍五入)后的String形式 变更为
取小数点后n位(四舍五入)后的String形式 这里的 n 为可变参数
此时就需要我们对之前的注解进行改造,改造形式如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsVO {
private Long id;
private String goodsName;
/**商品名*/
@DoubleFormat(value = 3)
private Double price;
}
注解中增加 value值, 通过给解析器提供value值来执行保留位数
思路确定,我们开始改造
二、改造注解
不废话,代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = DoubleFormatSerialize.class)
public @interface DoubleFormat {
int value() default 2;
}
这里给value值附了一个默认值2,因为取小数点后2位是一般业务常用的
接下来轮到重头戏,解析器改造
三、改造解析器
解析器中的关键是如何获取传入的value值
接下来就要介绍本文的重点了,ContextualSerializer是 Jackson 提供的另一个序列化相关的接口,它的作用是通过字段已知的上下文信息定制JsonSerializer,只需要实现createContextual方法即可:
implements ContextualSerializer:
extends JsonSerializer<Double> implements ContextualSerializer
实现 createContextual 方法
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
}
createContextual 方法解读
- createContextual可以获得字段的类型以及注解。
- createContextual方法只会在第一次序列化字段时调用(因为字段的上下文信息在运行期不会改变),所以不用担心影响性能。
- 代码中逻辑可编写为,当字段为Duoble类型并且拥有@DoubleFormat注解时,就取出注解中的value值,并创建定制的DoubleFormatSerialize,这样在serialize方法中便可以得到这个value值了。
改造之后的代码
public class DoubleFormatSerialize extends JsonSerializer<Double> implements ContextualSerializer {
/**
* 小数点后保留位数
*/
private int bitNum;
@Override
public void serialize(Double aDouble, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
StringBuilder pattern = new StringBuilder("#0.");
for (int i = 0; i < this.bitNum; i++) {
// 追加位数
pattern.append("0");
}
// 返回值
jsonGenerator.writeString(new DecimalFormat(pattern.toString()).format(aDouble));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
// 非 Double 直接跳过
if (Objects.equals(beanProperty.getType().getRawClass(), Double.class)) {
// 获取注解信息
DoubleFormat annotation = beanProperty.getAnnotation(DoubleFormat.class);
if (annotation == null) {
annotation = beanProperty.getContextAnnotation(DoubleFormat.class);
}
if (annotation != null) {
// 获得注解上的值并赋值
this.bitNum = annotation.value();
return this;
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
四、Controller测试
测试预期,我们在注解上输入自定义的值
@DoubleFormat(value = 3)
代表取小数点后3位
数据源如下:
return new GoodsVO(1001L, "《基督山伯爵》", 38.23456789D);
结果如下:
结果符合预期
总结
到此为止,类中属性注解讲解完毕,通过注解可以大大提升代码质量,避免反复造轮子的尴尬境地,防止需求变更时大量修改重复代码,解放双手,还请大家活学活用造福人类。
源码
Gitee:https://gitee.com/zhangxihuinan/annotation.git
参考
1、Jackson的ContextualSerializer应用场景