Java字典注解的简单实现(AOP)

需求来源

在开发过程中,必不可少会用到字典,例如,数据库字段性别字段可能是这样的:1:男;2:女,在数据存储的时候用1和2,但是在前端展示的时候需要使用男和女,而我们一般的开发方式有三种:

  1. 在sql查询的时候使用case函数判断,做字段值的转换
  2. 在查询出来结果后,遍历查询结果,根据数据库原值,新加上一个对应的字典值
  3. 返回原值,告诉调用方字典列表,让调用方自行转换

最终的结果大概是这样:

{

    "id": "100",

    "sex": "1",

    "sexText": "男"

}

方式1:需要手动写SQL,很繁琐

方式2:查出来之后需要再次便利一次,也要写代码,容易重复造轮子

方式3:对调用方来说不是很方便,并且后续加入字典含义需要改变,又需要同步给调用方,让他们去更新上线(虽然一般不会出现这样的情况)

鉴于这样的情况是非常常见的,这里我想针对方式2做一个封装,避免重复的去遍历、设置,所有有了这个字典转换组件。

实现思路

可以使用aop的思想,使用环绕通知,获取到方法的结果后,在对结果进行遍历处理

  1. 定义两个注解
    1. DictCovert 用于定义切入点
    2. Dict 真正的字典注解
  2. DictAspect 用来实现具体的注解逻辑
  3. DictUtil 封装字典翻译的逻辑,可提供手动方式

代码实现

定义DictCovert注解:

import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictCovert {
}

定义Dict注解:

import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {

    /**
     * 是否是本地字典
     * 本地字典:使用dictVal值做解析
     * 非本地字典:集合字典服务查询回来
     **/
    boolean isLocal() default true;

    /**
     * 字典存放后缀
     * 默认 "Text"
     * 例 原始字段名:type  翻译储存的字段名:typeText
     **/
    String suffix() default "Text";

    /**
     * 本地字典键值对,当isLocal=true时有效
     * 例如:1:草稿;2:已提交;3:已删除
     * 为了方便,使用的是中文的:和;
     **/
    String localDictVal() default "";

    /**
     * 在线字典编码,当isLocal=false时有效
     * 例如:onlineDictCode=SEX,则会调用 sysDictService.getDictItemMap("SEX");去获取字典值
     */
    String onlineDictCode() default "";

}

dictUtil工具类

注意:这里面有些类是需要替换成自己的,比如字典服务类,比如分页的对象等

import cn.hutool.core.util.ObjectUtil;
import com.xxx.*;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: v_luojinj
 * @since: 2023/2/2
 **/
@Component
public class DictUtil {

    /**
     * 使用个缓存,避免列表数据多次进行字典查询
     */
    private static ThreadLocal<Map<String,Map<String,String>>> cache  = ThreadLocal.withInitial(ConcurrentHashMap::new);

    public static Object parseResult(Object result) throws Exception {
        //判断空
        if (Objects.isNull(result)) {
            return null;
        }
        //判断结果类型
        if (result instanceof List) {
            //LIST类型
            List<Object> list = Lists.newArrayList();
            for (Object obj : (List<Object>) result) {
                list.add(parseDict(obj));
            }
            return list;
        } else if (result instanceof PageResult) {
            //自定义的分页返回结果集类型  实际结果在 list字段中。 处理和LIST一致
            PageResult pageResult = (PageResult) result;
            List<Object> list = Lists.newArrayList();
            for (Object obj : pageResult.getList()) {
                list.add(parseDict(obj));
            }
            //分页数据中 重新放入结果
            pageResult.setList(list);
            return pageResult;
        } else {
            //单实例对象类型
            return parseDict(result);
        }
    }

    /**
     * 字典转换
     * @param obj
     */
    private static Object parseDict(Object obj) throws NoSuchFieldException, IllegalAccessException {
        Field[] fields = obj.getClass().getDeclaredFields();
        //非空判断
        if (ObjectUtil.isEmpty(fields)) {
            return null;
        }
        for (Field field : fields) {
            //判断每一个字典是否有Dict注解
            if (Objects.nonNull(field.getAnnotation(Dict.class))) {
                handleDict(obj, field);
            }
        }
        return obj;
    }

    /**
     * 处理字典注解的字段
     * @param obj
     * @param field
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static void handleDict(Object obj, Field field) throws NoSuchFieldException, IllegalAccessException {
        Dict dict = field.getAnnotation(Dict.class);
        boolean local = dict.isLocal();
        if(local) {
            handleLocalDict(obj, field, dict);
        } else {
            handleOnlineDict(obj, field, dict);
        }
    }

    /**
     * 处理本地字典转换的逻辑
     * @param obj
     * @param field
     * @param dict
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static void handleLocalDict(Object obj, Field field, Dict dict) throws NoSuchFieldException, IllegalAccessException {
        String suffix = dict.suffix();
        String dictVal = dict.localDictVal();
        field.setAccessible(true);
        Object key = field.get(obj);
        if(StringUtils.isEmpty(dictVal) || Objects.isNull(key)){
            return;
        }
        Map<String, String> dictValMap = getLocalDictValMap(dictVal);
        Field name = obj.getClass().getDeclaredField(field.getName() + suffix);
        name.setAccessible(true);
        name.set(obj, dictValMap.get(key.toString()));
        name.setAccessible(false);
        field.setAccessible(false);
    }

    /**
     * 处理在线字典转换的逻辑
     * @param obj
     * @param field
     * @param dict
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static void handleOnlineDict(Object obj, Field field, Dict dict) throws NoSuchFieldException, IllegalAccessException {
        String suffix = dict.suffix();
        String dictCode = dict.onlineDictCode();
        field.setAccessible(true);
        Object key = field.get(obj);
        if(StringUtils.isEmpty(dictCode) || Objects.isNull(key)){
            return;
        }
        Map<String, String> dictValMap = getOnlineDictValMap(dictCode);
        Field name = obj.getClass().getDeclaredField(field.getName() + suffix);
        name.setAccessible(true);
        name.set(obj, dictValMap.get(key.toString()));
        name.setAccessible(false);
        field.setAccessible(false);
    }

    /**
     * 获取本地字典值列表,格式要求:1:草稿,2:已提交,3:已删除
     * @param dictVal 字典键值对
     * @return 字典列表
     */
    private static Map<String, String> getLocalDictValMap(String dictVal) {
        if (StringUtils.isEmpty(dictVal)) {
            return Collections.emptyMap();
        }
        Map<String, Map<String, String>> dictMap = cache.get();
        if(dictMap.containsKey(dictVal)){
            return dictMap.get(dictVal);
        }
        Map<String, String> valMap = new HashMap<>();
        String[] split = dictVal.split(";");
        for (String s : split) {
            String[] val = s.split(":");
            if (val.length != 2) {
                continue;
            }
            valMap.put(val[0], val[1]);
        }
        dictMap.put(dictVal,valMap);
        cache.set(dictMap);
        return valMap;
    }

    /**
     * 获取字典服务那边的字典列表
     * @param dictCode 字典编码
     * @return 字典列表
     */
    private static Map<String, String> getOnlineDictValMap(String dictCode) {
        if (StringUtils.isEmpty(dictCode)) {
            return Collections.emptyMap();
        }
        Map<String, Map<String, String>> dictMap = cache.get();
        if(dictMap.containsKey(dictCode)){
            return dictMap.get(dictCode);
        }
        SysDictService sysDictService = SpringContextHolder.getBean(SysDictService.class);
        Map<String, String> dictItemMap = sysDictService.getDictItemMap(dictCode);
        if(CollectionUtils.isEmpty(dictItemMap)){
            throw new ServiceException(ErrorCode.OBJECT_OR_PROPERTY_NOT_EXISTS,"未找到该字典编码:"+dictCode+",请检查");
        }
        dictMap.put(dictCode,dictItemMap);
        cache.set(dictMap);
        return dictItemMap;
    }

    /**
     * 清除缓存
     */
    public static void clearCache(){
        cache.remove();
    }
}

DictAsect切面:


import com.dg.ys.zsyz.annotation.dict.DictUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;

/**
 * @author: v_luojinj
 * @since: 2023/1/18
 **/
@Aspect
@Configuration
public class DictAspect {

    @Pointcut("@annotation(com.dg.ys.zsyz.annotation.dict.DictCovert)")
    public void dictCovert() {
    }

    @Around("dictCovert()")
    public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.执行原方法
        Object proceed = joinPoint.proceed();
        //2.拿到原方法的原返回值 调用parseResult进行字典转换
        Object result = DictUtil.parseResult(proceed);
        DictUtil.clearCache();
        return result;
    }
}

使用方式

Dict注解有两种使用方式:

1、isLocal = true,默认的方式,这种方式不用调用字典服务发起rpc请求,它通过解析localDictVal字段来做字典的翻译,格式见注释,一般情况下这种方式应该更常用;

2、isLocal = false,这种方式需要指定onlineDictCode,会调用字典服务发起rpc请求,获取字典服务那边的配置信息;

真正使用转换上也有两种使用方式:

1、使用DictCovert注解,在需要对结果对象做字典转换的接口或方法上添加该注解

2、使用DictUtil#parseResult 方法手动进行字典转换

详细步骤

通过注解作为aop切入点的方式:

1、在VO里需要做字典转换的字段添加@Dict,并新增用来存储的字段,如下面source和abc字段,source使用的是本地字典,abc使用的是在线字典

@Data

@ApiModel("xxx对象")

public class XxxVO {

    private String id;

    private String xxxName;

    @Dict(localDictVal = "1:男;2:女")

    private Integer sex;

    private String sexText;

    @Dict(isLocal = false,onlineDictCode = "SEX")

    private Integer oSex=1;

    private String oSexText;

}

2、在control接口处添加@DictCovert

   @PostMapping("/page")

   @DictCovert

   public PageResult<XxxVO> page(@RequestBody @Valid XxQO qo) {

       return XxxManager.listByPage(qo);

   }

使用工具方法的方式:

直接调用DictUtil#parseResult 该方法即可

注意

目前只对返回结果是java.util.List、PageResult、普通的VO类这些类型才可以做字典转换,并且不会做深度的字段转换,例如一个对象A里面的属性又是一个对象B,B里面使用Dict注解是识别不到的,这种可以在获取对象B的时候去做这个转换动作,而不是在外层的对象A上去做。

如需新增其他类型的转换可以在DictUtil#parseResult 里面添加

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Javashop开发规范V2.2 版本 说明 提交人 V1.0 初稿 定义了包名、异常、事务、和路径的规范 王峰 V2.0 1. 重新整理了命名规范 2. 增加常用命词对照表 王峰 V2.1 1.增加数据库操作的说明 2.增加常用方法介绍 王峰 V2.2 增加关于数据导入导出的说明 王峰 V2.3 增加校验使用说明(5.3章节) 王峰 1 命名规范 1.1 包命 1. 小写字母 2. 以com.enation.javashop开头 3. 组件以com.enation.compoent开头 1.2 类名 一、 action 以Action结尾,如:UserAction 二、 业务类 1.接口: 以I开头,以Manager结尾,如:IUserManager 2.实现类 以Manager结尾,如:UserManager 三、 组件类 以Component结尾,如:ShopEmailComponent 四、 挂件类 以Widget结尾,如:MemberAddressWidget 五、 插件类 以Plugin结尾,如:SendRegMailPlugin 1.3 变量/属性命名规则 1. 全部小写 2. 可用下划线连接 如:username、 userid 1.4 方法命名 1.4.1 Action类 add 到添加页 edit 到修改页 saveAdd 保存添加 saveEdit 保存修改 delete 删除 list 列表 1.4.2 业务类 add 添加 edit 修改 delete 删除 list 列表 get 读取详细 2 数据库开发规范 2.1 数据操作支持类 一、 业务类数据库调用 1.继承于BaseSupport 2.在spring文件中声明parent为baseSupport <bean id="xxxManagerImpl" class="xxx.xxx.xxx.XxxxManager" parent="baseSupport"/> 3.通过this.baseDaoSupport操作数据库 实际使用的是:com.enation.eop.sdk.database.BaseJdbcDaoSupport 此种操作示例: com.enation.javashop.core.service.impl.BrandManager 注意事项:见【BaseDaoSupport的意义和存在的问题】 二、 直接声明baseDaoSupport操作数据库 在一些挂件类中或某些特殊情况,可以直接需要直接声明baseDaoSupport 此种操作示例: com.enation.javashop.core.service.impl.batchimport.GoodsSpecImporter 注意事项:见【BaseDaoSupport的意义和存在的问题】 2.2 BaseDaoSupport的意义存在的问题 BaseDaoSupport对JdbcDaoSupport进行包装,通过 baseDBRouter 获取表名, 为什么要通过 baseDBRouter 来获取表名呢?Eop机制是支持SAAS(多租户)模式运行的,在SAAS会为每个用户提供如:es_goods_<userid>_<siteid>这样的表。 为了保证在单机版和SAAS模式中都运行正常,BaseDaoSupport将过滤sql中的相应表名。 但目前还只能支持简单的单表select、insert、update,对于多表的联合查询或更新不能支持。 在这种情况下,就需要通过 daoSupport(com.enation.framework.database.impl.JdbcDaoSupport)来操作,daoSupport不对sql进行任何更改,这时为了保证兼容saas模式兼容性,就要使用BaseSupport.getTableName(String tablename)方法 或baseDBRouter.getTableName(String tablename);来保证表名的正确。 (够混乱?真心希望出现一位大侠拯救这个状况,使basedaosupport可以处理所有情况的sql) 2.3 实体Bean和数据库表对照 Javahop数据库操作支持,将对象直接保存或修改,如: this.baseDaoSupport.insert("brand", brand); this.baseDaoSupport.update("brand", brand, "brand_id=" + brand.getBrand_id()); 规则为:实体中的属性名和数据库表的字段名相同,如: 对应的数据库字段: 2.4 注解的使用 2.4.1 @NotDbField 在某个实体Bean中,我们可能会有一些属性不对应数据库字段,这时我们需要在相应的 Geter方法中加上@NotDbField注解,以便使数据库机制知道这个字段不转为sql语句,如: private File file; @NotDbField public File getFile() { return file; } 2.4.2 @PrimaryKeyField 在实合格bean和数据库对照过程中,数据库机制需要识别主键,所以需要我们在主键的对应属性的Geter方法中加上@PrimaryKeyFiled注解,如: private Integer brand_id @PrimaryKeyField public Integer getBrand_id() { return brand_id; } 3 数据导入 3.1 导入接口 DBSolutionFactory.dbImport("file:com/enation/javashop/component/coupon/add.xml","es_"); 3.2 数据Xml文件说明 3.2.1 创建表 <action> <command>create</command> <table>tablename</table> <field> <name>id</name> <type>int</type> <size>8</size> <option>11</option> </field> <field> <name>name</name> <type>varchar</type> <size>255</size> <option>00</option> </field> </action> 3.2.2 删除表 <action> <command>drop</command> <table>tablename</table> </action> 3.2.3 添加、删除列 <action> <command>alter</command> <table>goods</table> <field type="add"> <name>isgroupbuy</name> <type>int</type> <size>1</size> <default>0</default> </field> <field type="drop"> <name>isgroup</name> </field> </action> 3.2.4 创建索引 <action> <command>index</command> <table>goods</table> <field > <name>goodsid</name> </field> </action> 3.2.5 删除索引 <action> <command>unindex</command> <table>goods</table> <field > <name>goodsid</name> </field> </action> 3.2.6 插入数据 <action> <command>insert</command> <table>es_adcolumn</table> <fields>acid,cname,width,height,atype,disabled</fields> <values>5,'列表页上部横幅','972px','67px',0,'false'</values> </action> 3.2.7 删除数据 暂未支持 3.2.8 更新数据 暂未支持 3.3 数据类型对照表 xml Mysql Oracle SqlServer int int NUMBER smallint int(1) smallint(1) NUMBER(2) int memo text CLOB text datetime datetime TIMESTAMP datetime long bigint NUMBER bigint decimal decimal NUMBER(20,2) decimal 4 数据导出 String[] tables = new String[1]; tables[0] = "es_auth_action"; DBSolutionFactory.dbExport(tables, false, "") 5 常用方法介绍 5.1 上下文获取 参见: http://www.javamall.com.cn/developer_help/index.php/常用方法 5.2 地区联动下拉框 1.如果是在jsp 中: <html:regionselect></html:regionselect> 2.如果是在Freemarker的html中: <#assign RegionSelect= "com.enation.app.base.component.widget.regions.RegionSelectDirective"?new()> <@RegionSelect /> 以上两种方式均支持以下参数: province_id:省id city_id:市id region_id:区id 如果指定上述参数,则默认选中 5.3 客户端校验 EOP自动为应用提供表单校验功能,通过指定form样式名和指定表单项特定属性的方式来完成。 5.3.1 示例 代码示例 [removed] function checkUserName(val){ if(val=='kingapex' ) return true; else return "用户名已存在"; } $.Validator.options={lang:{isrequired:'此项不能为空!'}}; [removed] <form class="validate"> 必须:<input type="text" name="test1" isrequired="true"> <br/> 整型:<input type="text" name="test2" isrequired="true" dataType="int"> <br/> 浮点型:<input type="text" name="test3" dataType="float"> <br/> 邮件:<input type="text" name="test4" isrequired="true" dataType="email"> <br/> 日期:<input type="text" name="test5" isrequired="true" dataType="date"> <br/> 电话号码:<input type="text" name="test6" isrequired="true" dataType="tel_num"> <br/> 手机:<input type="text" name="test7" isrequired="true" dataType="mobile"> <br/> 邮政编码:<input type="text" name="test8" isrequired="true" dataType="post_code"> <br/> 网址:<input type="text" name="test9" isrequired="true" dataType="url"> <br/> 自定义函数:<input type="text" name="test10" isrequired="true" fun="checkUserName"> <br/> <input type="submit" value="确定" /> </form> 5.3.2 参数说明: 5.3.2.1 isrequired 为true则为必填项,不指定或指为false则为非必填项。 5.3.2.2 dateType 指定校验特殊类型,支持的类型见下表: 类型 说明 int 整数 float 浮点数 email 邮件格式 date 日期格式 tel_num 电话格式 mobile 手机格式 post_code 邮编 url 网址 5.3.2.3 fun 自定义校验函数,返回真则通过校验,返回假或字串则校验失败,返回的字串会出现在失败提示框中。 5.3.2.4 动态绑定校验函数 通过 setValidator实现 $("#region_id").setValidator(function(){ var value = $("#region_id").val(); if( value=="" || value=="0" ) return "地区信息不完整"; else return true; }); 5.3.2.5 提示器 提示器用于显示校验的结果,如果不指定默认会在校验的控件后面创建一个span做为提示器。 可以通过两种方式指定提示器: 一、在控件中声明tiper属性: <input type="text" name="username" tiper="#name_tiper" /> 注:tiper指定的是jquery的一个选择器表达式。 二、动态指定: $("#region_id").setTiper($("#name_tiper")); 注:此时指定的是jquery对象,而不是一个表达式 5.3.2.6 手动调用: $("#siteForm").checkall(); 6 常用字典对照表 6.1 常用值 名称 值 说明 返回结果 0:失败 1:成功 适用于: 1. 客户端json返回值 2. 服务器端方法返回值 3. 数据库是否的标识值 性别 0:女 1:男 6.2 常用名称 名称 英文 商品 goods 货品 product 规格 spec 订单 order 会员 member 积分 point 购物车 cart 结算 checkout 订单 order 品牌 brand 分类 cat 优惠劵 coupon 支付 payment 团购 groupbuy 虚拟 virtual 发票 receipt 属性 prop 参数 param 标签 tag 地区 region 7 异常 1. 提供统一的异常处理机制 2. 底层级别的类不处理异常,只管向上抛出异常,且统一抛出RuntimeExcepton 8 事务处理 1. 数据库统一使用Srping AOP事务 2. 采用注解方式: 在接口处: @Transactional(propagation = Propagation.isrequired) 9 路径的规范 返回的路径变量最后不带 ‘/’ 如:String path =”user/1”; 使用者: path = path+”/”+myVar; 10 样式/HTML规范 10.1 前台分页 <div class="page"><span class="info">共41条记录</span> <span class="info">1/3</span> <ul><li><a class="selected">1</a></li> <li><a href="search-cat-4-page-2.html" class="unselected">2</a></li> <li><a href="search-cat-4-page-3.html" class="unselected">3</a></li> <li><a href="search-cat-4-page-2.html" class="unselected">>></a></li> <li><a href="search-cat-4-page-3.html" class="unselected">>|</a></li> </ul></div>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值