不积跬步,无以至千里。不积小流,无以成江海。

通常我们有这种需求,某一个字段是由前台维护的:编码 <==> 含义,类似一个键值对。含义是可能会变化的,但编码是固定的,一般我们都会在数据库中直接存储编码,但数据返回给前台显示时需要展示为含义。当然我们可以在从数据库查询时去join到该值的含义一起返回,这里提供另一种方式来实现这个功能。

实现思路

主要是使用注解加切面。在数据返回前通过切面把被注解的字段替换成该值的含义。

这里使用性别字段,键值关系为:

M - 男

W - 女

新建注解

新建一个注解用于标识字段需要被切面处理

1
2
3
4
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SexValue {
}

新建第二个注解,作为一个切点。

1
2
3
4
5
@Target(value = ElementType.METHOD )
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ProcessResult {
}

注解参数含义:

  • @interface : 表示定义一个注解

  • @Target 表示该注解可以用于什么地方,可能的ElementType参数有:

    • CONSTRUCTOR:构造器的声明
    • FIELD:域声明(包括enum实例)
    • LOCAL_VARIABLE:局部变量声明
    • PACKAGE:包声明
    • PARAMETER:参数声明
    • TYPE:类、接口(包括注解类型)或enum声明
  • @Retention 表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:

    • SOURCE:注解将被编译器丢弃
    • CLASS:注解在class文件中可用,但会被VM丢弃
    • RUNTIME:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息
  • @Document 将注解包含在Javadoc中

  • @Inherited 允许子类继承父类中的注解

Entity

实体类如下,在 sex 字段上添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Person {

private Long id;

private String name;

private Integer age;

@SexValue
private String sex;

private String address;

private String phoneNum;

private LocalDateTime createdDate;

private LocalDateTime lastUpdateDate;

private Long objectVersionNumber;

public void createBefore(){
this.createdDate = LocalDateTime.now();
this.lastUpdateDate = LocalDateTime.now();
this.objectVersionNumber = 1L;
}

public void updateBefore(Long objectVersionNumber){
this.lastUpdateDate = LocalDateTime.now();
this.objectVersionNumber = objectVersionNumber + 1L;
}
}

Controller

Controller 中,把需要翻译的方法加上注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("/v1/hjwjw/person")
public class PersonController {

private IPersonService personService;

public PersonController(IPersonService personService) {
this.personService = personService;
}

@GetMapping
@ProcessResult
public ResponseEntity<List<Person>> query(){
return ResponseEntity.ok(personService.queryPerson());
}

@PostMapping
@ProcessResult
public ResponseEntity<Person> createPerson(@RequestBody Person personVO){
return ResponseEntity.ok(personService.createPerson(personVO));
}

@PutMapping
@ProcessResult
public ResponseEntity<Person> updatePerson(@RequestBody Person personVO){
return ResponseEntity.ok(personService.updatePerson(personVO));
}

@DeleteMapping("/{personId}")
public ResponseEntity delPerson(@PathVariable("personId") Long personId){
personService.delPerson(personId);
return ResponseEntity.ok(HttpStatus.OK);
}

}

新建切面

新建一个切面类,在Controller中加了 @ProcessResult 注解的方法,在返回前会进入切面进行处理。

返回的Object需要判断是否为集合,并把其父类字段都需要遍历查找是否有添加@SexValue注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Component
@Aspect
public class SexAspect {

private static final Logger LOGGER = LoggerFactory.getLogger(SexAspect.class);

@AfterReturning(value = "@annotation(processResult)",returning = "result")
public Object aftreReturning(JoinPoint joinPoint, ProcessResult processResult,Object result) throws IllegalAccessException {
LOGGER.info(joinPoint.toString());
LOGGER.info("<===================Aspect======================>");
LOGGER.info(result.toString());

if (result == null){
return null;
}
if (result instanceof ResponseEntity){
Object body = ((ResponseEntity<?>) result).getBody();
if (body == null){
return null;
}
if (body instanceof Collection ){
for (Object obj : (Collection<?>) body){
//处理
processObj(obj);
}
}else {
//处理
processObj(body);
}
}else if (result instanceof Collection){
//处理
for (Object obj : (Collection<?>) result){
//处理
processObj(obj);
}
}else {
processObj(result);

}
return result;
}

private void processObj(Object obj) throws IllegalAccessException {
//取出Obj所有 Field,以及父类 Field
List<Field> fieldList = new ArrayList<>();
Class<?> tempClass = obj.getClass();
while (tempClass != null){
fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
tempClass = tempClass.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);

//遍历Field,查找添加 @SexValue 注解的字段 并 做翻译
for (Field field : fields){
if (field.isAnnotationPresent(SexValue.class)){
field.setAccessible(true);
String fieldValue = String.valueOf(field.get(obj));
LOGGER.info("fieldValue:{}",fieldValue);
if ("M".equals(fieldValue)){
field.set(obj,"男");
}else {
field.set(obj,"女");
}
}
}
}
}

这里只是做了简单的值转换。至于如何获取到对应的含义,建议把配置的值集缓存到Redis,这样在切面处理时可以根据编码从 Redis 中直接取出含义进行替换。