单例+工厂+配置文件做了一个可切换底层的 工具类封装。
1、需求
项目中会大量用到工具类,比如字符串工具类、集合工具类、http工具类、object工具类等。有很多开源包,比较常用的有 apache 的工具包、hutool 工具包。我希望在这些第三方包的基础上做一层封装,可以随时切换工具包的底层实现。防止某个开源包有bug、不维护了,能够方便更换。
以封装 string工具类、coll 集合工具类为例子。
2、构思
有不同的底层实现,究竟选哪一种底层实现,肯定是需要开发的时候传进入,才能进行判断选择。
我想实现的是一处修改,全局都不用修改。所以这个控制选取底层实现的变量应该项目启动之前就确定了。
于是把这个变量放在配置类中。
我只关心工具类的使用,并不关心创建,我希望我给你一个变量,你动态创建我需要的对象,我随处可用。很明显,将类的创建与使用进行解耦的工厂模式很适合这里。
3、实战
3.1 配置类
配置 type: 你是用 hutool、apache、system【自己实现】
包路径 packagePath:真正做实现的工具包的路径。
注意:我使用的是静态变量注入,需要用set方法,直接@value是不可以的。还有其他方法自行百度。
JAVA代码:
/**
* @author xgz
*/
@Slf4j
@Component
@ConfigurationProperties(prefix = "custom.utils")
public class UtilsConfig {
/** 工具底层类型 **/
public static String type;
/** 真正干活的实现类包路径 **/
public static String packagePath;
public void setType(String type){
UtilsConfig.type = type;
log.info("注入静态变量type={}成功", type);
}
public void setPackagePath(String packagePath){
UtilsConfig.packagePath = packagePath;
log.info("packagePath={}成功", packagePath);
}
}
配置文件:
custom: # 工具类底层配置 utils: # 底层类型 type: hutool # 真正实现类所在的包 package-path: com.xgzit.common.thirdpack.impl
3.2、常量、枚举
工具类底层实现类型 的枚举,为了和配置类中的配置进行匹配。
import lombok.Getter;
import java.util.Objects;
public enum UtilsBaseEnum {
/** 阿帕奇底层实现 **/
APACHE("apache", "阿帕奇 底层实现"),
/** hutool底层实现 **/
HUTOOL("hutool", "hutool 底层实现"),
/** 不使用工具自己底层实现 **/
SYSTEM("system", "本系统自己的底层实现")
;
@Getter
private String value;
@Getter
private String label;
UtilsBaseEnum(String value, String label){
this.value = value;
this.label = label;
}
/** 根据值获取枚举 **/
public static UtilsBaseEnum getEnum(String value){
if (Objects.isNull(value)){
throw new RuntimeException("枚举值不能为空");
}
for (UtilsBaseEnum item : UtilsBaseEnum.values()) {
if (item.value.equalsIgnoreCase(value)){
return item;
}
}
throw new RuntimeException("请输入正常的枚举值");
}
}
工具类型枚举 有字符串工具类、集合工具类、对象工具类等。
import lombok.Getter;
import java.util.Objects;
public enum UtilsTypeEnum {
/** 字符串工具类 **/
STRING("String", "字符串工具类"),
/** 集合工具类 **/
COLL("Coll", "集合工具类"),
/** 对象工具类 **/
OBJECT("Object", "对象工具类")
;
@Getter
private String value;
@Getter
private String label;
UtilsTypeEnum(String value, String label){
this.value = value;
this.label = label;
}
/** 根据值获取枚举 **/
public static UtilsTypeEnum getEnum(String value){
if (Objects.isNull(value)){
throw new RuntimeException("枚举值不能为空");
}
for (UtilsTypeEnum item : UtilsTypeEnum.values()) {
if (item.value.equalsIgnoreCase(value)){
return item;
}
}
throw new RuntimeException("请输入正常的枚举值");
}
}
3.3、抽取工具类的公共方法作为接口
因为有2种类型的工具类,字符串工具类和 集合类型工具类。
抽取字符串工具类的通用方法,作为接口
public interface StringUtilsMethods {
boolean isBlank(String str);
boolean isNotBlank(String str);
boolean isEmpty(String str);
boolean isNotEmpty(String str);
}
抽取集合工具类的通用方法,作为接口
/**
* 集合工具类方法
* @author xgz
*/
public interface CollUtilsMethods {
boolean isEmpty(Collection coll);
boolean isNotEmpty(Collection coll);
}
3.4、不同的底层实现
以 apache工具类为底层实现的 字符串工具类
/**
* 以 apache工具类为底层实现的 字符串工具类
* @author xgz
*/
public class ApacheStringUtils implements StringUtilsMethods {
public static final String type = "APACHE_STRING";
@Override
public boolean isBlank(String str) {
System.out.println("apach 底层 str");
return StringUtils.isBlank(str);
}
@Override
public boolean isNotBlank(String str) {
return StringUtils.isNotBlank(str);
}
@Override
public boolean isEmpty(String str) {
return StringUtils.isEmpty(str);
}
@Override
public boolean isNotEmpty(String str) {
return StringUtils.isNotEmpty(str);
}
}
hutool工具为底层实现的字符串工具类
/**
* 以 hutool 工具为底层实现的字符串工具类
* @author xgz
*/
public class HutoolStringUtils implements StringUtilsMethods {
public static final String type = "HUTOOL_STRING";
@Override
public boolean isBlank(String str) {
System.out.println("hutool的底层实现 str ");
return CharSequenceUtil.isBlank(str);
}
@Override
public boolean isNotBlank(String str) {
return CharSequenceUtil.isNotBlank(str);
}
@Override
public boolean isEmpty(String str) {
return CharSequenceUtil.isEmpty(str);
}
@Override
public boolean isNotEmpty(String str) {
return CharSequenceUtil.isNotEmpty(str);
}
}
如果需要拓展,写上各种底层实现的工具类就是了。
工具类中的 type 字段的值: 必须是 底层实现类型 + "_" + 工具类型枚举 的组合,建议大写,其实无所谓,因为匹配时,无论是配置文件的,还是枚举类的,都会先全部转成大写,再进行匹配。
它的作用是生成映射,就是类型与实现类之间的映射。
import cn.hutool.core.collection.CollUtil;
import com.xgzit.common.thirdpack.factory.coll.CollUtilsMethods;
import java.util.Collection;
/**
* 以 hutool 工具为底层实现的字符串工具类
* @author xgz
*/
public class HutoolCollUtils implements CollUtilsMethods {
public static final String type = "HUTOOL_COLL";
@Override
public boolean isEmpty(Collection coll) {
System.out.println("hutool的底层实现 HUTOOL_COLL ");
return CollUtil.isEmpty(coll);
}
@Override
public boolean isNotEmpty(Collection coll) {
System.out.println("hutool的底层实现 HUTOOL_COLL ");
return CollUtil.isNotEmpty(coll);
}
}
阿帕奇集合工具底层实现
import com.xgzit.common.thirdpack.factory.coll.CollUtilsMethods;
import org.apache.commons.collections4.CollectionUtils;
import java.util.Collection;
public class ApacheCollUtils implements CollUtilsMethods {
public static final String type = "APACHE_COLL";
@Override
public boolean isEmpty(Collection coll) {
System.out.println("apach 底层 coll");
return CollectionUtils.isEmpty(coll);
}
@Override
public boolean isNotEmpty(Collection coll) {
System.out.println("apach 底层 coll");
return CollectionUtils.isNotEmpty(coll);
}
}
3.5、获取类型与实现类映射的工具类
原理是包扫描,包路径是 配置文件packagePath 的值。如果需要扫描子包,还需要修改一下代码。
@Slf4j
public class UtilsMapping {
private static HashMap<String, Class> utilsMap = new HashMap<>();
// 通过包扫描获取包下所有.class文件
private static Class[] getClassByPackage(String packageName) {
try {
Enumeration<URL> resources = UtilsMapping.class.getClassLoader()
.getResources(packageName.replaceAll("\\.", "/"));
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String[] file = new File(url.getFile()).list();
List<String> aClass = Arrays.stream(file)
.filter(item -> item.endsWith("class"))
.collect(Collectors.toList());
Class[] classList = new Class[aClass.size()];
for (int i = 0; i < aClass.size(); i++) {
classList[i] = Class
.forName(packageName + "." + aClass.get(i)
.replaceAll("\\.class", ""));
}
return classList;
}
} catch (Exception e) {
e.printStackTrace();
}
return new Class[0];
}
public static Map<String, Class> getMapping(){
Class[] classByPackage = getClassByPackage(UtilsConfig.packagePath);
for (Class aClass : classByPackage) {
try {
Field type = aClass.getDeclaredField("type");
String o = (String) type.get(null);
if (Objects.nonNull(o)) {
utilsMap.put(o.toUpperCase(), aClass);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
log.info("{}没有type字段",aClass.getSimpleName());
}
}
return utilsMap;
}
}
这个工具的getMapping方法会得到这样的映射, key是type,value是实现类。
关键代码是 utilsMap.put(o.toUpperCase(), aClass); 意味着key会先变成大写,做了一个忽略大小写的处理。
3.6 工厂接口
只有一个方法,获取工具类。
如果是很多个方法,方法还返回接口,就变成抽象工厂,这里不需要,只需要创建一种工具就ok。
/**
* 工具工厂 接口 获取各种类型的工具 字符串String、coll类型
* @author xgz
*/
public interface UtilsFactory<T> {
T getUtil();
}
3.7 工厂实现类
我们最终要获取的是 字符串工具类、集合工具类,并不关心底层实现。这个工厂实现类就是 根据我们传入的配置值,创建对应的工具类。工具类创建一次就可以了,采用线程安全的懒汉式单例。
@Component
public class StringUtilFactory implements UtilsFactory<StringUtilsMethods> {
private static final String TYPE = UtilsTypeEnum.STRING.getValue().toUpperCase();
private volatile static StringUtilsMethods strUtils = null;
@Override
public StringUtilsMethods getUtil() {
if (strUtils == null){
synchronized (StringUtilFactory.class){
if (strUtils == null){
UtilsBaseEnum anEnum = UtilsBaseEnum.getEnum(UtilsConfig.type);
Map<String, Class> mapping = UtilsMapping.getMapping();
Class aClass = mapping.get(anEnum.getValue().toUpperCase() + "_" + TYPE);
try {
strUtils = (StringUtilsMethods)aClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
return strUtils;
}
}
主要逻辑是 根据配置类的 type 去 枚举类里面找,防止配置文件乱写。找不到会报错。也可以改成默认值形式。找到之后 和工厂自身的静态变量 TYPE 进行拼接,就是 UtilsMapping 的key的样子,再去这个映射类中找到对应的类,返回。用单例的话就不会重复创建对象了。
@Component
public class CollUtilFactory implements UtilsFactory<CollUtilsMethods> {
private String TYPE = UtilsTypeEnum.COLL.getValue().toUpperCase();
private volatile static CollUtilsMethods coll;
@Override
public CollUtilsMethods getUtil() {
if (coll == null) {
synchronized (CollUtilFactory.class) {
UtilsBaseEnum anEnum = UtilsBaseEnum.getEnum(UtilsConfig.type);
Map<String, Class> mapping = UtilsMapping.getMapping();
Class aClass = mapping.get(anEnum.getValue().toUpperCase() + "_" + TYPE);
try {
coll = (CollUtilsMethods) aClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return coll;
}
}
3.8 使用
首先要在配置文件配好。
然后在需要的地方注入,要什么工具类,注入什么工厂。
// 注入
private final StringUtilFactory stringUtilFactory;
// 先获取工具类,再调用工具类的方法
stringUtilFactory.getUtil().isBlank("测试字符串");
4、拓展
切换 底层实现只需 配置文件修改 type 的值。
拓展情况1:
增加一种已实现的工具类底层实现,比如 xxx实现的字符串工具类。
只需要写一个实现类实现 字符串的通用方法,type为 xxx_STRING
然后在 底层实现类型 枚举 加上这个底层实现。 即可。
拓展情况2:
如果是新加入 比如说对象类型工具类。现在只有字符串类型、集合类型工具类。
那就需要仿造 字符串类型或者集合类型写。
1、对象类型工具类的公共方法 抽取成接口
2、需要对象类型工具类的 不同底层实现类,实现上面的接口
3、写一个对象工具工厂实现类,实现工厂接口。类似 StringUtilFactory,用单例提供具体的实现类
4、在 工具类型枚举 加上这种类型的工具,如果新增了底层实现,要在 底层实现枚举类 新增一条枚举。
最后附上全部代码图: