• 实例介绍基于项目依赖包选择具体实现类


    最近遇到一个需求场景,开源的工具包,新增了一个高级特性,会依赖json序列化工具,来做一些特殊操作;但是,这个辅助功能并不是必须的,也就是说对于使用这个工具包的业务方而言,正常使用完全不需要json相关的功能;如果我强引用某个json工具,一是对于不适用高级特性的用户而言没有必要;二则是我引入的json工具极有可能与使用者的不一致,会增加使用者的成本

    因此我希望这个工具包对外提供时,并不会引入具体的json工具依赖;也就是说maven依赖中的设置为provided;具体的json序列化的实现,则取决于调用方自身引入了什么json工具

    那么可以怎么实现上面这个方式呢?

    1. 任务说明

    上面的简单的说了一下我们需要做的事情,接下来我们重点盘一下,我们到底是要干什么

    核心诉求相对清晰

    1. 不强引入某个json工具
    2. 若需要使用高级特性,则直接使用当前环境中已集成的json序列化工具;若没有提供,则抛异常,不支持

    对于上面这个场景,常年使用Spring的我们估计不会陌生,Spring集成了很多的第三方开源组件,根据具体的依赖来选择最终的实现,比如日志,可以是logback,也可以是log4j;比如redis操作,可以是jedis,也可以是lettuce

    那么Spring是怎么实现的呢?

    2.具体实现

    在Spring中有个注解名为ConditionalOnClass,表示当某个类存在时,才会干某些事情(如初始化bean对象)

    它是怎么是实现的呢?(感兴趣的小伙伴可以搜索一下,或者重点关注下 SpringBootCondition 的实现)

    这里且抛开Spring的实现姿势,我们采用传统的实现方式,直接判断是否有加载对应的类,来判断有没有引入相应的工具包

    如需要判断是否引入了gson包,则判断ClassLoader是否有加载com.google.gson.Gson

    1. public static boolean exist(String name) {
    2. try {
    3. return JsonUtil.class.getClassLoader().loadClass(name) != null;
    4. } catch (Exception e) {
    5. return false;
    6. }
    7. }

    上面这种实现方式就可以达到我们的效果了;接下来我们参考下Spring的ClassUtils实现,做一个简单的封装,以判断是否存在某个类

    1. // 这段代码来自Spring
    2. // Source code recreated from a .class file by IntelliJ IDEA
    3. // (powered by FernFlower decompiler)
    4. //
    5. import java.lang.reflect.Array;
    6. import java.util.HashMap;
    7. import java.util.Map;
    8. /**
    9. * @author Spring
    10. */
    11. public abstract class ClassUtils {
    12. private static final Map<String, Class<?>> primitiveTypeNameMap = new HashMap(32);
    13. private static final Map<String, Class<?>> commonClassCache = new HashMap(64);
    14. private ClassUtils() {
    15. }
    16. public static boolean isPresent(String className) {
    17. try {
    18. forName(className, getDefaultClassLoader());
    19. return true;
    20. } catch (IllegalAccessError var3) {
    21. throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + var3.getMessage(), var3);
    22. } catch (Throwable var4) {
    23. return false;
    24. }
    25. }
    26. public static boolean isPresent(String className, ClassLoader classLoader) {
    27. try {
    28. forName(className, classLoader);
    29. return true;
    30. } catch (IllegalAccessError var3) {
    31. throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + var3.getMessage(), var3);
    32. } catch (Throwable var4) {
    33. return false;
    34. }
    35. }
    36. public static Class<?> forName(String name, ClassLoader classLoader) throws ClassNotFoundException, LinkageError {
    37. Class<?> clazz = resolvePrimitiveClassName(name);
    38. if (clazz == null) {
    39. clazz = (Class) commonClassCache.get(name);
    40. }
    41. if (clazz != null) {
    42. return clazz;
    43. } else {
    44. Class elementClass;
    45. String elementName;
    46. if (name.endsWith("[]")) {
    47. elementName = name.substring(0, name.length() - "[]".length());
    48. elementClass = forName(elementName, classLoader);
    49. return Array.newInstance(elementClass, 0).getClass();
    50. } else if (name.startsWith("[L") && name.endsWith(";")) {
    51. elementName = name.substring("[L".length(), name.length() - 1);
    52. elementClass = forName(elementName, classLoader);
    53. return Array.newInstance(elementClass, 0).getClass();
    54. } else if (name.startsWith("[")) {
    55. elementName = name.substring("[".length());
    56. elementClass = forName(elementName, classLoader);
    57. return Array.newInstance(elementClass, 0).getClass();
    58. } else {
    59. ClassLoader clToUse = classLoader;
    60. if (classLoader == null) {
    61. clToUse = getDefaultClassLoader();
    62. }
    63. try {
    64. return Class.forName(name, false, clToUse);
    65. } catch (ClassNotFoundException var9) {
    66. int lastDotIndex = name.lastIndexOf(46);
    67. if (lastDotIndex != -1) {
    68. String innerClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1);
    69. try {
    70. return Class.forName(innerClassName, false, clToUse);
    71. } catch (ClassNotFoundException var8) {
    72. }
    73. }
    74. throw var9;
    75. }
    76. }
    77. }
    78. }
    79. public static Class<?> resolvePrimitiveClassName(String name) {
    80. Class<?> result = null;
    81. if (name != null && name.length() <= 8) {
    82. result = (Class) primitiveTypeNameMap.get(name);
    83. }
    84. return result;
    85. }
    86. public static ClassLoader getDefaultClassLoader() {
    87. ClassLoader cl = null;
    88. try {
    89. cl = Thread.currentThread().getContextClassLoader();
    90. } catch (Throwable var3) {
    91. }
    92. if (cl == null) {
    93. cl = ClassUtils.class.getClassLoader();
    94. if (cl == null) {
    95. try {
    96. cl = ClassLoader.getSystemClassLoader();
    97. } catch (Throwable var2) {
    98. }
    99. }
    100. }
    101. return cl;
    102. }
    103. }

    工具类存在之后,我们实现一个简单的json工具类,根据已有的json包来选择具体的实现

    1. public class JsonUtil {
    2. private static JsonApi jsonApi;
    3. private static void initJsonApi() {
    4. if (jsonApi == null) {
    5. synchronized (JsonUtil.class) {
    6. if (jsonApi == null) {
    7. if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", JsonUtil.class.getClassLoader())) {
    8. jsonApi = new JacksonImpl();
    9. } else if (ClassUtils.isPresent("com.google.gson.Gson", JsonUtil.class.getClassLoader())) {
    10. jsonApi = new GsonImpl();
    11. } else if (ClassUtils.isPresent("com.alibaba.fastjson.JSONObject", JsonUtil.class.getClassLoader())) {
    12. jsonApi = new JacksonImpl();
    13. } else {
    14. throw new UnsupportedOperationException("no json framework to deserialize string! please import jackson|gson|fastjson");
    15. }
    16. }
    17. }
    18. }
    19. }
    20. /**
    21. * json转实体类,会根据当前已有的json框架来执行反序列化
    22. *
    23. * @param str
    24. * @param t
    25. * @param
    26. * @return
    27. */
    28. public static <T> T toObj(String str, Class<T> t) {
    29. initJsonApi();
    30. return jsonApi.toObj(str, t);
    31. }
    32. public static <T> String toStr(T t) {
    33. initJsonApi();
    34. return jsonApi.toStr(t);
    35. }
    36. }

    上面的实现中,根据已有的json序列化工具,选择具体的实现类,我们定义了一个JsonApi接口,然后分别gson,jackson,fastjson给出默认的实现类

    1. public interface JsonApi {
    2. T toObj(String str, Class clz);
    3. String toStr(T t);
    4. }
    5. public class FastjsonImpl implements JsonApi {
    6. public T toObj(String str, Class clz) {
    7. return JSONObject.parseObject(str, clz);
    8. }
    9. public String toStr(T t) {
    10. return JSONObject.toJSONString(t);
    11. }
    12. }
    13. public class GsonImpl implements JsonApi {
    14. private static final Gson gson = new Gson();
    15. public T toObj(String str, Class t) {
    16. return gson.fromJson(str, t);
    17. }
    18. public String toStr(T t) {
    19. return gson.toJson(t);
    20. }
    21. }
    22. public class JacksonImpl implements JsonApi{
    23. private static final ObjectMapper jsonMapper = new ObjectMapper();
    24. public T toObj(String str, Class clz) {
    25. try {
    26. return jsonMapper.readValue(str, clz);
    27. } catch (Exception e) {
    28. throw new UnsupportedOperationException(e);
    29. }
    30. }
    31. public String toStr(T t) {
    32. try {
    33. return jsonMapper.writeValueAsString(t);
    34. } catch (Exception e) {
    35. throw new UnsupportedOperationException(e);
    36. }
    37. }
    38. }

    最后的问题来了,如果调用方并没有使用上面三个序列化工具,而是使用其他的呢,可以支持么?

    既然我们定义了一个JsonApi,那么是不是可以由用户自己来实现接口,然后自动选择它呢?

    现在的问题就是如何找到用户自定义的接口实现了

    3. 扩展机制

    对于SPI机制比较熟悉的小伙伴可能非常清楚,可以通过在配置目录META-INF/services/下新增接口文件,内容为实现类的全路径名称,然后通过 ServiceLoader.load(JsonApi.class) 的方式来获取所有实现类

    除了SPI的实现方式之外,另外一个策略则是上面提到的Spring的实现原理,借助字节码来处理(详情原理后面专文说明)

    当然也有更容易想到的策略,扫描包路径下的class文件,遍历判断是否为实现类(额外注意jar包内的实现类场景)

    接下来以SPI的方式来介绍下扩展实现方式,首先初始化JsonApi的方式改一下,优先使用用户自定义实现

    1. private static void initJsonApi() {
    2. if (jsonApi == null) {
    3. synchronized (JsonUtil.class) {
    4. if (jsonApi == null) {
    5. ServiceLoader<JsonApi> loader = ServiceLoader.load(JsonApi.class);
    6. for (JsonApi value : loader) {
    7. jsonApi = value;
    8. return;
    9. }
    10. if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", JsonUtil.class.getClassLoader())) {
    11. jsonApi = new JacksonImpl();
    12. } else if (ClassUtils.isPresent("com.google.gson.Gson", JsonUtil.class.getClassLoader())) {
    13. jsonApi = new GsonImpl();
    14. } else if (ClassUtils.isPresent("com.alibaba.fastjson.JSONObject", JsonUtil.class.getClassLoader())) {
    15. jsonApi = new JacksonImpl();
    16. } else{
    17. throw new UnsupportedOperationException("no json framework to deserialize string! please import jackson|gson|fastjson");
    18. }
    19. }
    20. }
    21. }
    22. }

    对于使用者而言,首先是实现接口

    1. package com.github.hui.quick.plugin.test;
    2. import com.github.hui.quick.plugin.qrcode.util.json.JsonApi;
    3. public class DemoJsonImpl implements JsonApi {
    4. @Override
    5. public T toObj(String str, Class clz) {
    6. // ...
    7. }
    8. @Override
    9. public String toStr(T t) {
    10. // ...
    11. }
    12. }

    接着就是实现定义, resources/META-INF/services/ 目录下,新建文件名为 com.github.hui.quick.plugin.qrcode.util.json.JsonApi

    内容如下

    1. com.github.hui.quick.plugin.test.DemoJsonImpl

    4. 小结

    主要介绍一个小的知识点,如何根据应用已有的jar包来选择具体的实现类的方式;本文介绍的方案是通过ClassLoader来尝试加载对应的类,若能正常加载,则认为有;否则认为没有;这种实现方式虽然非常简单,但是请注意,它是有缺陷的,至于缺陷是啥...

    除此之外,也可以考虑通过字节码的方式来判断是否有某个类,或者获取某个接口的实现;文中最后抛出了一个问题,如何获取接口的所有实现类

    常见的方式有下面三类(具体介绍了SPI的实现姿势,其他的两种感兴趣的可以搜索一下)

    • SPI定义方式
    • 扫描包路径
    • 字节码方式(如Spring,如Tomcat的@HandlesTypes)

     

  • 相关阅读:
    算法自学__树链剖分
    docker构建FreeSWITCH编译环境及打包
    基础会计学名词解释
    怎样给Ubuntu系统安装vmware-tools
    vue3-element-plus使用
    《Windows API每日一练》4.4 绘制填充区域
    Hazelcast系列(八):数据结构
    01【数据库的介绍】
    关于环2数字资产html网页设计
    【Linux系统编程(文件编程)】之创建、打开文件
  • 原文地址:https://blog.csdn.net/weixin_62421895/article/details/126521399