【Java】Springboot + Redis +(AOP & 响应外切)切面实现字典翻译

 

使用案例演示:

先开发了一个简单的Demo:

普通DTO类注解翻译的字段和翻译来源

 

 

在需要翻译的方法上注解@Translate

 

 

接口返回结果:

 

 

框架思路:

1、标记的注解需要通过AOP切面在调用的时候处理翻译

2、翻译的来源是Redis的缓存,需要有数据来源,应用启动之后就需要初始化

 

一、配置Redis

pom.xml的相关依赖:

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.3.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.4</version>
        </dependency>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring.boot.version>2.3.10.RELEASE</spring.boot.version>
        <durid.version>1.2.14</durid.version>
    </properties>

  

Redis的yml配置:

spring:
  redis:
    host: 192.168.124.8
    database: 0
    timeout: 3000
    password: 123456
    jedis:
      pool:
        max-active: 29
        max-wait: -1
        max-idle: 10
        min-idle: 0

 

RedisTemplate配置类:

package cn.cloud9.server.struct.redis;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.SerializeWriter;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月10日 下午 10:20
 */
@Configuration
public class RedisConfiguration {

    /**
     * 改用fastjson redis序列化,请删除redis数据后使用此序列化
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, ?> redisTemplate(@Lazy RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        GenericToStringSerializer<String> stringRedisSerializer = new GenericToStringSerializer<>(String.class);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        FastJsonRedisSerializer<?> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
        FastJsonConfig fastJsonConfig = fastJsonRedisSerializer.getFastJsonConfig();
        SerializeConfig serializeConfig = fastJsonConfig.getSerializeConfig();

        /* 加入的LocalDateTime序列化,也可以不加(但是要用@JSONField(format = "yyyy-MM-dd HH:mm:ss"))格式化 */
        serializeConfig.put(LocalDateTime.class, (serializer, object, fieldName, fieldType, features) -> {
            SerializeWriter out = serializer.out;
            if (object == null) {
                out.writeNull();
                return;
            }
            out.writeString(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format((LocalDateTime) object));
        });
        serializeConfig.put(LocalDate.class, (serializer, object, fieldName, fieldType, features) -> {
            SerializeWriter out = serializer.out;
            if (object == null) {
                out.writeNull();
                return;
            }
            out.writeString(DateTimeFormatter.ofPattern("yyyy-MM-dd").format((LocalDate) object));
        });
        serializeConfig.put(LocalTime.class, (serializer, object, fieldName, fieldType, features) -> {
            SerializeWriter out = serializer.out;
            if (object == null) {
                out.writeNull();
                return;
            }
            out.writeString(DateTimeFormatter.ofPattern("HH:mm:ss").format((LocalTime) object));
        });
        serializeConfig.put(Date.class, (serializer, object, fieldName, fieldType, features) -> {
            SerializeWriter out = serializer.out;
            if (object == null) {
                out.writeNull();
                return;
            }
            out.write("\"" + DateUtil.format(((Date)object),"yyyy-MM-dd HH:mm:ss") + "\"");
        });
        fastJsonConfig.setSerializeConfig(serializeConfig);
        fastJsonConfig.setFeatures(Feature.SupportAutoType);
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteClassName);
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

 

二、数据源来源获取

数据源配置:

spring:
  datasource:
    url: jdbc:mysql://192.168.124.8:3308/tt?serverTimeZone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

  

创建字典表的DTO,Mapper

package cn.cloud9.server.struct.dict.dto;

import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 09:31
 */
@Data
@TableName("system_dict")
public class DictDTO {

    @TableId(value = "DICT_ID", type = IdType.AUTO)
    private Integer dictId;
    @TableField("DICT_CODE")
    private Integer dictCode;
    @TableField("DICT_TYPE")
    private String dictType;
    @TableField("DICT_ALIAS")
    private String dictAlias;
    @TableField("DICT_NAME")
    private String dictName;
    @TableField("DICT_TYPE_NAME")
    private String dictTypeName;
    @TableField("DICT_TYPE_ALIAS")
    private String dictTypeAlias;
    @TableField("DICT_PARENT_ID")
    private String dictParentId;
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    @TableField("GEN_TIME")
    private LocalDateTime genTime;
}

 

Mapper配置一个自定义查询SQL的方法:

package cn.cloud9.server.struct.dict.mapper;

import cn.cloud9.server.struct.dict.dto.DictDTO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface DictMapper extends BaseMapper<DictDTO> {

    @Select("${SQL}")
    List<DictDTO> queryUsingCustomSql(@Param("SQL") String sql);
}

  

一般来说是加载字典表放入缓存中,但是还有类似行政区域表,也是需要缓存放入的

字典表:

SELECT * FROM `system_dict`

非字典,但是也可以按照字典表结构存储的表:

SELECT `NAME` AS `DICT_CODE`, 'AB_WORD' AS `DICT_TYPE`, `MEANING` AS `DICT_NAME` FROM abridge_word

只要字段适配,同样可以按照字典装载

 

我们可以有若干个需要装载的表,那需要写在配置文件中解除硬编码控制:

package cn.cloud9.server.struct.dict.cache;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

/**
 * 缓存配置读取类,用于读取需要Redis装载的缓存
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 10:10
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "cache")
public class CacheProperty {
    private Map<String, String> sqlMap;
}

  

则yml的配置声明如下:

cache:
  sql-map:
    default: SELECT * FROM `system_dict`
#    ab-word: SELECT `NAME` AS `DICT_CODE`, 'AB_WORD' AS `DICT_TYPE`, `MEANING` AS `DICT_NAME` FROM abridge_word

 

在需要加载的时候可以遍历配置Bean的Map,依次SQL查询需要装载的数据

// 1、注入配置Bean 和 mapper
    @Resource
    private CacheProperty cacheProperty;

    @Resource
    private DictMapper dictMapper;

// 2、方法中获取map交给mapper执行
        /* 读取配置文件的缓存SQL */
        final Map<String, String> sqlMap = cacheProperty.getSqlMap();

        for (String sqlKey : sqlMap.keySet()) {
            final String sql = sqlMap.get(sqlKey);
            final List<DictDTO> dictList = baseMapper.queryUsingCustomSql(sql)
        }

  

三、缓存抽象与实现

缓存的功能抽象成接口,最主要的三个功能:

1、初始化

2、按字典编码获取翻译名称

3、按字典类别获取这个类别的集合

package cn.cloud9.server.struct.dict.cache;

import cn.cloud9.server.struct.dict.dto.DictDTO;

import java.util.List;

/**
 * 缓存服务接口
 *
 */
public interface CacheService {

    void initializeCacheDataToRedis();

    String findNameFromRedis(String dictCode);

    List<DictDTO> findListFromRedis(String dictCate);
}

  

存入的Redis的结构是采用Hash,即 Key + Hkey + Hvalue

Hvalue又分成了两种类型,String 和 List<DictDTO>

第一种,获取翻译名称的时候,存入需要定义一个根Key, 和组合的Hkey

规则是这样: 根Key写死在Bean中不变,组合的Hkey = 表名(配置SQL的Key键) + 分隔符 +  字典类别 + 分割符 + 字典编号,Hvalue 是字典名称

第二种,需要获取某一个类别的集合,用于下拉列表,或者在前台翻译

规则是这样: 根Key写死在Bean中不变,组合的Hkey =  表名(配置SQL的Key键) + 分隔符 +  字典类别, Hvalue 是这个类别的集合

 

了解上述规则后,这个缓存服务接口,交给DictService来实现:

package cn.cloud9.server.struct.dict.service;

import cn.cloud9.server.struct.dict.cache.CacheProperty;
import cn.cloud9.server.struct.dict.cache.CacheService;
import cn.cloud9.server.struct.dict.dto.DictDTO;
import cn.cloud9.server.struct.dict.mapper.DictMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * 字典服务
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 09:21
 */
@Slf4j
@Service
public class DictService extends ServiceImpl<DictMapper, DictDTO> implements CacheService {

    public static final String KEY_LISTS = "REDIS-LISTS-CACHE";
    public static final String KEY_MAP = "REDIS-MAPS-CACHE";
    public static final String SEPARATOR = "@";

    @Resource
    private CacheProperty cacheProperty;
    @Resource
    private StringRedisTemplate stringTemplate;
    @Resource
    private RedisTemplate<String, Map<String, String>> mapTemplate;


    /**
     * 缓存初始化处理
     */
    @Override
    public void initializeCacheDataToRedis() {
        final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash();
        /* 清空缓存 */
        stringTemplate.delete(KEY_MAP);
        stringTemplate.delete(KEY_LISTS);

        /* 读取配置文件的缓存SQL */
        final Map<String, String> sqlMap = cacheProperty.getSqlMap();
        /* 准备缓存结构容器, 并装载数据 */
        Map<String, String> mapTank = new ConcurrentHashMap<>();
        Map<String, List<DictDTO>> listTank = new ConcurrentHashMap<>();
        for (String sqlKey : sqlMap.keySet()) {
            final String sql = sqlMap.get(sqlKey);
            final List<DictDTO> dictList = baseMapper.queryUsingCustomSql(sql);
            for (DictDTO dict : dictList) {
                final Integer dictCode = dict.getDictCode();
                final String dictName = dict.getDictName();
                final String dictType = dict.getDictType();

                /* 装载 key -> h-key -> h-value */
                final String mapKey = sqlKey + SEPARATOR + dictType + SEPARATOR + dictCode;
                mapTank.put(mapKey, dictName);

                /* 装载 key -> h-key -> h-list */
                final String listKey = sqlKey + SEPARATOR + dictType;
                List<DictDTO> cateList = listTank.get(listKey);
                if (CollectionUtils.isEmpty(cateList)) {
                    cateList = new ArrayList<>();
                    listTank.put(listKey, cateList);
                }
                cateList.add(dict);
            }
        }

        /* 装填到Redis中 */
        hashOps.putAll(KEY_MAP, mapTank);
        hashOps.putAll(KEY_LISTS, listTank);

        log.info("Redis 缓存装载完毕 ...... ");
    }

    /**
     *
     * @param dictCode 格式:sqlKey@字典类别@字典编码
     * @return 字典名称 找不到为null
     */
    @Override
    public String findNameFromRedis(String dictCode) {
        final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash();
        final Object o = hashOps.get(KEY_MAP, dictCode);
        final boolean isEmpty = Objects.isNull(o);
        return !isEmpty ? (String) o : "";
    }

    /**
     *
     * @param dictCate 格式:sqlKey@字典类别
     * @return 字典类别集合 找不到为空集合
     */
    @SuppressWarnings("unchecked")
    @Override
    public List<DictDTO> findListFromRedis(String dictCate) {
        final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash();

        final Object o = hashOps.get(KEY_LISTS, dictCate);
        final boolean isEmpty = Objects.isNull(o);
        return !isEmpty ? (List<DictDTO>) o : Collections.EMPTY_LIST;
    }
}

  

四、解决初始化加载的问题

初始化的实现有了,Bean也有了,那怎么才能让应用一启动的时候就开始执行呢?

而且执行一次通常是类静态资源调用的做法,于是就用到了 

import org.springframework.context.ApplicationContextAware;

 

所有Bean装入Spring完毕后会执行Aware,通过Aware可以获取容器上下文对象

通过上下文对象,根据类型和Bean名称,可以静态的获取对应的Bean,

有了DictServiceBean之后,就可以调用初始化了

package cn.cloud9.server.struct.spring;

import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

/**
 * Spring上下文持有器类,用于静态方式获取Bean实例
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 11:04
 */
@Service
@Lazy(value = false)
public class SpringContextHolder implements ApplicationContextAware {
    /**
     * spring上下文
     */
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        SpringContextHolder.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 获取bean
     * @param name  bean名称
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name){
        return (T) applicationContext.getBean(name);
    }

    /**
     * 获取bean
     * @param requiredType bean类型
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> requiredType){
        return applicationContext.getBean(requiredType);
    }

    /**
     * 获取bean
     * @param name  bean名称
     * @param requiredType  bean类型
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> requiredType){
        return applicationContext.getBean(name,requiredType);
    }

}

  

五、管理缓存

初始化是可以复用的,涉及字典相关的数据一旦更新发生变化,Redis的缓存也需要同步

这里最简单的做法就是刷新处理,结合上面的Bean持有器类,可以这样实现:

package cn.cloud9.server.struct.dict.cache;

import cn.cloud9.server.struct.spring.SpringContextHolder;

/**
 * 缓存管理器类,用于刷新缓存
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 11:08
 */
public class CacheManager {

    public static void refreshCache() {
        final CacheService cacheService = SpringContextHolder.getBean("dictService", CacheService.class);
        new Thread(cacheService::initializeCacheDataToRedis).start();
    }
}

 

放在Boot主启动类完成后调用:

package cn.cloud9.server;

import cn.cloud9.server.struct.dict.cache.CacheManager;
import cn.cloud9.server.struct.validator.EnableFormValidator;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月06日 下午 04:18
 */
@MapperScan(basePackages = "cn.cloud9.server.*")
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
        CacheManager.refreshCache();
    }
}

 

重启运行看看能不能触发加载

 

 查看Redis是否按照规则存入了字典:

 

六、设计注解

两个问题:在哪里翻译?  翻译什么?

对应两个注解:@Translate @DictFrom

 

这里@Translate注解 加了类对象声明,好像不需要,先无视把

声明在方法上标记,用来给AOP定位目标方法

package cn.cloud9.server.struct.dict.annotation;

import java.lang.annotation.*;

/**
 * 标记此注解时翻译PO对象
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Translate {

    /**
     * 翻译的DTO类
     */
    Class<?> dtoClass();
}

 

@DictFrom,用来标记翻译字段

package cn.cloud9.server.struct.dict.annotation;

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictFrom {
    /* 翻译的来源表 */
    String srcTable() default "default";

    /* 翻译的指定类别 */
    String srcCate();

    /* 翻译的来源字段 */
    String srcField();

    /* 翻译的字段是否是多个的, 默认单个 */
    boolean isMulti() default false;

    /* 如果是多个的,每个值的分隔符是? */
    String separator() default ",";
}

  

七、编写字典翻译切面:

因为是个Demo, 切面这里的作用就是判断类型,翻译单独交给反射工具类来完成了

暂时只考虑单个Bean, 集合接口和翻页对象三种,也没有考虑嵌套Bean的情况

package cn.cloud9.server.struct.dict.aspect;

import cn.cloud9.server.struct.dict.annotation.Translate;
import cn.cloud9.server.struct.dict.reflect.ReflectUtil;
import cn.cloud9.server.test.model.DictAspectModel;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月12日 下午 10:02
 */
@Slf4j
@Aspect
@Component
public class DictAspect {

    @Resource
    private ReflectUtil reflectUtil;

    @Pointcut(value = "@annotation(translate)", argNames = "translate")
    public void doTranslate(Translate translate) {
    }


    @AfterReturning(pointcut = "doTranslate(translate)", returning = "result", argNames = "point,result,translate")
    public Object translation(final JoinPoint point, Object result, Translate translate) throws Throwable {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        final Class<?> aClass = translate.dtoClass();

        final boolean isCollection = result instanceof Collection;
        final boolean isPage = result instanceof IPage;
        final boolean isTargetClass =  aClass.equals(result.getClass());

        if (!isCollection && !isPage && !isTargetClass) return result;
        else if (isCollection) {
            List<Object> list = (List<Object>) result;
            if (CollectionUtils.isEmpty(list)) return result;
            for (Object row : list) reflectUtil.translateDTO(row, aClass);
        } else if (isPage) {
            IPage<Object> page = (IPage<Object>) result;
            if (CollectionUtils.isEmpty(page.getRecords())) return result;
            final List<Object> records = page.getRecords();
            for (Object record : records) reflectUtil.translateDTO(record, aClass);
        } else if (isTargetClass) {
            reflectUtil.translateDTO(result, aClass);
        }


        return result;
    }
}

  

八、反射工具类:

反射工具类为了优化反射操作,这里用了hutool的工具

package cn.cloud9.server.struct.dict.reflect;

import cn.cloud9.server.struct.dict.annotation.DictFrom;
import cn.cloud9.server.struct.dict.annotation.Translate;
import cn.cloud9.server.struct.dict.service.DictService;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.Objects;
import cn.hutool.core.bean.BeanUtil;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 08:28
 */
@Component
public class ReflectUtil {

    @Resource
    private DictService dictService;

    /**
     *
     * @param result
     * @param aClass
     */
    public void translateDTO(Object result, Class<?> aClass)  {
        /* 获取这个类下的所有字段 */
        final Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {

            /* 获取类上的@Translate注解 */
            final DictFrom dictFrom = field.getAnnotation(DictFrom.class);
            /* 如果没有此注解则跳过 */
            if (Objects.isNull(dictFrom)) continue;

            /* 获取声明的字典来源信息 */
            final String srcTable = dictFrom.srcTable();
            final String srcCate = dictFrom.srcCate();
            final String srcField = dictFrom.srcField();
            final boolean isMulti = dictFrom.isMulti();
            final String separator = dictFrom.separator();

            /* 取出目标对象对应字段的值 */
            final Object fieldValue = BeanUtil.getFieldValue(result, srcField);
            /* 如果没有值则跳过, 或者值类型不是字符串或者整形 */
            if (Objects.isNull(fieldValue) ) continue;
            else if (!(fieldValue instanceof String) && !(fieldValue instanceof Integer)) continue;

            if (!isMulti) {
                /* 调用Redis资源开始翻译 */
                final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + String.valueOf(fieldValue);
                final String translateName = dictService.findNameFromRedis(key);
                /* 赋值翻译字段 */
                BeanUtil.setFieldValue(result, field.getName(), translateName);
            } else {
                final String[] split = ((String)fieldValue).split(separator);
                final StringBuilder builder = new StringBuilder();
                for (int i = 0; i < split.length; i++) {
                    final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + split[i].trim();
                    if (i == split.length - 1) {
                        final String fromRedis = dictService.findNameFromRedis(key);
                        builder.append(fromRedis);
                    } else {
                        final String fromRedis = dictService.findNameFromRedis(key);
                        builder.append(fromRedis);
                        builder.append(separator);
                    }
                }
                /* 赋值翻译字段 (多个) */
                BeanUtil.setFieldValue(result, field.getName(), builder.toString());
            }

        }
    }
}

  

 

九、补充嵌套Bean的情况:

因为有第一个翻译的方法逻辑,后面实现起来就很容易了

只需要在前面判断当前字段是不是集合或者翻译类型的,然后逐个遍历判断类型

当然这个判断没有那么严谨,一般开发的情况不会装载一般数据类型,如果确实碰到了,可以结合实际情况再添加判断补充处理

最后递归调用翻译方法就可以了

package cn.cloud9.server.struct.dict.reflect;

import cn.cloud9.server.struct.dict.annotation.DictFrom;
import cn.cloud9.server.struct.dict.annotation.Translate;
import cn.cloud9.server.struct.dict.service.DictService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import cn.hutool.core.bean.BeanUtil;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月13日 下午 08:28
 */
@Component
public class ReflectUtil {

    @Resource
    private DictService dictService;

    /**
     * 翻译DTO
     * @param result
     */
    public void translateDTO(Object result)  {
        /* 获取这个类下的所有字段 */
        final Field[] declaredFields = result.getClass().getDeclaredFields();
        for (Field field : declaredFields) {

            /* 处理嵌套在目标对象类中的集合类型翻译 */
            final Object fieldValue = BeanUtil.getFieldValue(result, field.getName());
            final boolean isCollection = fieldValue instanceof Collection;
            final boolean isPage = fieldValue instanceof IPage;
            if (isCollection) {
                Collection<Object> list = (Collection<Object>) fieldValue;
                if (CollectionUtils.isEmpty(list)) continue;
                for (Object row : list) {
                    if (row.getClass().isPrimitive()) continue;
                    this.translateDTO(row);
                }
            } else if (isPage) {
                IPage<Object> page = (IPage<Object>) fieldValue;
                if (CollectionUtils.isEmpty(page.getRecords())) continue;
                final List<Object> records = page.getRecords();
                for (Object record : records) {
                    if (record.getClass().isPrimitive()) continue;
                    this.translateDTO(record);
                }
            }

            /* 获取类上的@Translate注解 */
            final DictFrom dictFrom = field.getAnnotation(DictFrom.class);
            /* 如果没有此注解则跳过 */
            if (Objects.isNull(dictFrom)) continue;

            /* 获取声明的字典来源信息 */
            final String srcTable = dictFrom.srcTable();
            final String srcCate = dictFrom.srcCate();
            final String srcField = dictFrom.srcField();
            final boolean isMulti = dictFrom.isMulti();
            final String separator = dictFrom.separator();

            /* 取出目标对象对应字段的值 */
            final Object resultFieldValue = BeanUtil.getFieldValue(result, srcField);

            /* 如果没有值则跳过, 或者值类型不是字符串或者整形 */
            if (Objects.isNull(resultFieldValue) ) continue;
            else if (!(resultFieldValue instanceof String) && !(resultFieldValue instanceof Integer)) continue;

            if (!isMulti) {
                /* 调用Redis资源开始翻译 */
                final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + String.valueOf(resultFieldValue);
                final String translateName = dictService.findNameFromRedis(key);
                /* 赋值翻译字段 */
                BeanUtil.setFieldValue(result, field.getName(), translateName);
            } else if (resultFieldValue instanceof String && isMulti) {
                /* 按照注解声明的分割符对目标值进行切割,如果标签 */
                final String[] split = ((String)resultFieldValue).split(separator);

                /* 对切片逐一翻译,再拼接回来 */
                final StringBuilder builder = new StringBuilder();
                for (int i = 0; i < split.length; i++) {
                    final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + split[i].trim();
                    if (i == split.length - 1) {
                        final String fromRedis = dictService.findNameFromRedis(key);
                        builder.append(fromRedis);
                    } else {
                        final String fromRedis = dictService.findNameFromRedis(key);
                        builder.append(fromRedis);
                        builder.append(separator);
                    }
                }
                /* 赋值翻译字段 (多个) */
                BeanUtil.setFieldValue(result, field.getName(), builder.toString());
            }

        }
    }
}

  

这里重写测试Controller的方法验证一下我们的递归:

package cn.cloud9.server.test.controller;

import cn.cloud9.server.struct.dict.annotation.Translate;
import cn.cloud9.server.struct.dict.dto.DictDTO;
import cn.cloud9.server.struct.dict.mapper.DictMapper;
import cn.cloud9.server.test.model.DictAspectModel;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月12日 下午 10:15
 */
@Slf4j
@RestController(value = "testDictController")
@RequestMapping("/test/dict")
public class DictController {

    @Resource
    private DictMapper dictMapper;

    @Translate
    @GetMapping("/tran")
    public List<DictAspectModel> translateAspectTest() {

        /* 测试集合内的DTO能否翻译 */
        List<DictAspectModel> models = new ArrayList<>();
        for (int i = 1; i < 2; i++) {
            final DictAspectModel model = new DictAspectModel();
            model.setDictCode(1006000 + i);
            model.setMovieType("1013013 , 1013015 , 1013017 , 1013020 ");
            models.add(model);
        }

        /* 设置嵌套DTO,测试能否翻译内嵌对象 */
        final DictAspectModel model = new DictAspectModel();
        model.setDictCode(1006003);
        model.setMovieType("1013013 , 1013015 , 1013017 , 1013020 ");
        final ArrayList<DictAspectModel> innerList = new ArrayList<>();
        innerList.add(model);
        models.get(0).setModels(innerList);

        log.info("翻译切面之前:models {}", JSON.toJSONString(models));

        return models;
    }

    /**
     * 测试我们编写的SQL执行是否有效
     * @param sql 自定义SQL
     * @return 字典集合
     */
    @GetMapping("/sql")
    public List<DictDTO> getDictListBySql(@RequestBody String sql) {
        return dictMapper.queryUsingCustomSql(sql);
    }

}

 

Postman请求结果:

可以看到内嵌的集合DTO也能被翻译出来

 

十、使用ResponseBodyAdvice取代AOP

AOP切入点是针对方法层级的,如果需要扩大切点颗粒细度,还是使用响应外切来完成

 

1、默认所有模型实体响应给前端就是需要翻译的

只有特别情况才不需要翻译,通过此注解标记来控制

可以定义在Controller上或者方法上

package cn.cloud9.server.struct.dict.annotation;

import java.lang.annotation.*;

/**
 * 禁用字典翻译标记
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DisableTranslate {
}

 

2、切点判断逻辑:

package cn.cloud9.server.struct.dict.hook;

import cn.cloud9.server.struct.dict.annotation.DisableTranslate;
import cn.cloud9.server.struct.dict.reflect.ReflectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
 * @author OnCloud9
 * @description 使用响应外切实现翻译入口
 * @project tt-server
 * @date 2022年11月25日 下午 07:19
 */
@Order(2)
@ControllerAdvice(annotations = RestController.class)
public class DictAdvice implements ResponseBodyAdvice<Object> {

    @Resource
    private ReflectUtil reflectUtil;

    /**
     * 增加入口颗粒度, 可标注在类或方法上控制是否翻译
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        final Class<DisableTranslate> dtClass = DisableTranslate.class;

        /* 1、判断是否在类上标记 */
        DisableTranslate disableTranslate = methodParameter.getContainingClass().getAnnotation(dtClass);
        boolean isMarkOnClass = Objects.nonNull(disableTranslate);

        /* 2、判断是否在方法上标记 */
        disableTranslate = methodParameter.getMethod().getAnnotation(dtClass);
        boolean isMarkOnMethod = Objects.nonNull(disableTranslate);

        /* 3、只要在类或者方法上标记,则表示不使用翻译 */
        return !(isMarkOnClass || isMarkOnMethod);
    }

    @SuppressWarnings("all")
    @Override
    public Object beforeBodyWrite(
            Object result,
            MethodParameter methodParameter,
            MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> aClass,
            ServerHttpRequest serverHttpRequest,
            ServerHttpResponse serverHttpResponse
    ) {
        /* 是否为空 */
        final boolean isEmpty = Objects.isNull(result);
        if (isEmpty) return result;
        /* 返回的结果类型是否为基本类型 */
        final boolean isPrimitive = result.getClass().isPrimitive();
        if (isPrimitive) return result;
        /* 返回的结果类型是否为集合 */
        final boolean isCollection = result instanceof Collection;
        /* 返回的结果类型是否为翻页对象 */
        final boolean isPage = result instanceof IPage;

        if (isCollection) {
            Collection<Object> list = (Collection<Object>) result;
            if (CollectionUtils.isEmpty(list)) return result;
            for (Object row : list) reflectUtil.translateDTO(row);
        } else if (isPage) {
            IPage<Object> page = (IPage<Object>) result;
            if (CollectionUtils.isEmpty(page.getRecords())) return result;
            final List<Object> records = page.getRecords();
            for (Object record : records) reflectUtil.translateDTO(record);
        } else {
            reflectUtil.translateDTO(result);
        }

        return result;
    }
}

  

  

 

posted @ 2022-11-14 23:41  emdzz  阅读(1362)  评论(0编辑  收藏  举报