工作中常常会用到文件加载,然后又经常忘记,印象不深,没有系统性研究过,从最初的war包项目到现在的springboot项目,从加载外部文件到加载自身jar包内文件,也发生了许多变化,这里开一贴,作为自己的备忘录,也希望能给广大 java

coder 带来帮助。

一、目标

通过此文,能熟知普通war包项目目录内、jar包自身内文件的加载方式。

二、文件定位

2.1 WAR 包项目

为什么先说war包项目,war包项目部署到Web容器里后 ,会被解压,所以文件读取方式,和在ide里面读取是类似的。

读取文件,首先要定位文件,定位到文件之后才能读取。

定位文件,java常用的有两种,分别是

  • URL Class.getResource(String name)
  • URL ClassLoader.getResource(String name)

这里的参数 name ,就是咱们认为的路径,官方对这个参数名的描述是:

name of the desired resource

渴望得到的资源的名字

URL 则是资源的定位,可以得到资源所在路径。

URL 可以是不同的资源,通过其字段 protocol 来区分是哪种类型资源,取值有:

  • ftp
  • nntp
  • http
  • file
  • jar

感兴趣的同学可以自行了解 URL 的定义

2.1.1 Class.getResource(String name)

通过class实例获得资源的定位,传入参数有如下查找方式:

  • / 开头,则从 classPath 即运行的 class 文件所在的项目的 ***/classes/ 目录下找起
  • 非以 / 开头的,则从当前class所在路径下找起

验证:

先上项目结构图

验证代码

public class ClassResource {
public static void main(String[] args) {
ClassResource classResource = new ClassResource();
classResource.resWithInstance("");
classResource.resWithInstance("/");
classResource.resWithInstance("ClassResource.class");
classResource.resWithInstance("/ClassResource.class");
classResource.resWithInstance("/1.txt");
} public void resWithInstance(String path) {
URL resource = this.getClass().getResource(path);
print(resource, path);
} private static void print(URL resource, String path) {
try {
System.out.println("ClassResource 根据目录[" + String.format("%-20s", path) + "] 获取路径为 " + resource);
} catch (Exception e) {
System.out.println("ClassResource 根据目录[" + path + "] 获取路径出错,错误原因:" + e.getMessage());
}
} }

我们传入了5个参数,分别是

  • 空字符串
  • /
  • 当前类文件名
  • / + 当前类文件名
  • / + 项目 resources 目录下的 1.txt 文件

运行结果如下:

结果分析:

  1. 空字符串

    定位为当前类路径

  2. /

    定位为 classPath 路径

  3. 当前类文件名

    定位为当前类文件所在路径,成功定位到文件

  4. / + 当前类文件名

    定位不到文件

  5. / + 项目 resources 目录下的 1.txt 文件

    定位为 resoures/1.txt,因为编译后 resources 目录里的文件都移动到了 classPath 路径下,所以也成功定位

4 的错误原因很明显,因为 classPath 路径下没有名叫 ClassResource.class 的文件,所以定位不到

总结:

使用 class 查找文件,以 / 开头的文件名,是从 classPath 目录下找,否则从当前类文件目录下找

2.1.2 ClassLoader.getResource(String name)

通过 classLoader 实例获得资源的定位,传入参数仅有如下查找方式:

  • classPath 路径下找起

验证:

验证代码

public class ClassLoaderResource {

    public static void main(String[] args) {
ClassLoaderResource classLoaderResource = new ClassLoaderResource();
classLoaderResource.resWithInstance("");
classLoaderResource.resWithInstance("/");
classLoaderResource.resWithInstance("ClassLoaderResource.class");
classLoaderResource.resWithInstance("/ClassLoaderResource.class");
classLoaderResource.resWithInstance("1.txt");
classLoaderResource.resWithInstance("/1.txt");
} public void resWithInstance(String path) {
URL resource = this.getClass().getClassLoader().getResource(path);
print(resource, path);
} private static void print(URL resource, String path) {
try {
System.out.println("ClassLoaderResource 根据目录[" + String.format("%-26s", path) + "] 获取路径为" + resource);
} catch (Exception e) {
System.out.println("ClassLoaderResource 根据目录[" + path + "]获取路径出错,错误原因:" + e.getMessage());
}
}
}

我们传入了6个参数,分别是

  • 空字符串
  • /
  • 当前类文件名
  • / + 当前类文件名
  • 1.txt
  • /1.txt

运行结果如下:

结果分析:

  1. 空字符串

    定位为 classPath 路径

  2. /

    定位不到

  3. 当前类文件名

    定位不到

  4. / + 当前类文件名

    定位不到

  5. resources目录下的 1.txt 文件名

    定位为 resoures/1.txt,因为编译后 resources 目录里的文件都移动到了 classPath 路径下,成功定位

  6. / + resources目录下的 1.txt 文件名

    定位不到

3 的错误原因很明显,因为 classPath 路径下没有名叫 ClassResource.class 的文件,所以定位不到

2、4、6 的错误原因是因为以 / 开头,这里先记着:

/ 开头的都会定位不到,但是参数中可以带有 / 来表示下一级路径

如:查找 Main.class 的参数此处应写为 com/yx/jtest/Main.class

总结:

使用 classLoader 查找文件,总是从 classPath 目录下找起,且不能以 / 开头

2.1.3 Class.getResource 与 ClassLoader.getResource 的异同原因

对于相同的开头字符 /空字符串 为什么两种方式的执行结果不一样呢

来分析下 class.getResource 源码

public class Class {
public java.net.URL getResource(String name) {
name = resolveName(name); // ①
ClassLoader cl = getClassLoader0();
if (cl == null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
}

可以看到在进行 ① 转换资源名称后,内部还是调用了 classLoader.getResource 方法。

那么异同的奥秘就都在这个第一行里的 resolveName(name) 方法里了

来看 resolveName(name)

public class Class {
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
*/
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
} // 这里的baseName类似 com.foo.Bar 之类的形式
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) { // 拼name,把包名称拼上,如 "com/foo/" + "/" + "Bar1"
// 就是获取当前类目录下的路径名
name = baseName.substring(0, index).replace('.', '/')
+ "/" + name;
}
} else {
name = name.substring(1);
}
return name;
}
}

可以看到:

  1. 如果不以 / 开头,就返回 当前类所在目录 + 资源名
  2. 否则返回 / 后面的字符串
  3. 总结就是该方法把相对路径转换为了基于 classPath 的绝对路径

在经过资源名称处理后,就跟 classLoader.getResource 的规则一样了。

这里处理 / 符号也间接说明了 classLoader.getResource 不再接受 / 开头的资源名称,因为它把 / 当成了路径分隔符,下面是官方的参数说明

The name of a resource is a '/'-separated path name that identifies the resource.

资源的名称是一个“/”分隔的路径名,用于标识资源。

所以两者的异同点在于:

class.getResource 先进行了 / 符号开头的路径的预处理,使之转换为了基于 classPath 的绝对路径,再调用 classLoader.getResource 的方法

classLoader.getResource 只接受基于 classPath 的绝对路径,并不再接受以 /开头的路径,此时 "" 空字符串则代表 classPath 路径,而非 class.getResource/

2.2 JAR包项目

当项目为jar项目时,加载的方式变了,主要有

  1. classPath 路径由 file 目录变成了 jar文件,这影响到资源的定位方式,而且不再支持获取当前 classPath 路径
  2. URLClassPath 加载资源时候由 FileLoader 变成了 JarLoader,这影响到资源对特殊符号的处理方式
  3. 定位内部文件的URL协议由 file 变成了 jar,这影响到资源文件的读取方式

先来看打包成jar后的运行情况,这次使用另外一个类去写测试,该类直接调用上面的演示方法

代码:

public class Main {
public static void main(String[] args) {
ClassResource classResource = new ClassResource();
classResource.resWithInstance("");
classResource.resWithInstance("/");
classResource.resWithInstance("ClassResource.class");
classResource.resWithInstance("/ClassResource.class");
classResource.resWithInstance("1.txt");
classResource.resWithInstance("/1.txt"); ClassLoaderResource classLoaderResource = new ClassLoaderResource();
classLoaderResource.resWithInstance("");
classLoaderResource.resWithInstance("/");
classLoaderResource.resWithInstance("ClassResource.class");
classLoaderResource.resWithInstance("/ClassResource.class");
classLoaderResource.resWithInstance("1.txt");
classLoaderResource.resWithInstance("/1.txt");
}
}

运行结果:

分析:

class.getResource()

  1. 空字符串仍代表当前类路径,以非 / 开头的资源名称都会从当前类路径下找起。记得没有打包时候的规则吗,没错,这里的空格又被转换成了当前类路径,然后调用的 classLoader.getResource() 方法
  2. / 仍代表应用 classPath 路径,以 / 开头的资源名称都会从应用 classPath 路径下找起,找资源名为 / 后面的字符的资源。但是如果只传 / 则定位不到,不能输出 classPath 路径

classLoader.getResource()

  1. 还是不接受 / 开头的资源名称,所有 / 开头的资源名称都会返回为null,定位不到
  2. 空字符串原本代表 classPath 路径,这里不再支持
  3. / 开头的资源从应用 classPath 路径下找起

其他

  1. classPath 由文件夹变成了jar文件
  2. URL协议由 file: 变成了 jar:file:

三、文件加载

上一章节,我们已经知道了 classclassLoader 定位资源的异同,和在打成 jar 包之后的变化。

现在定位文件已经做到了,这里不再区分究竟是 class 定位的文件还是classLoader 定位的文件,本章节就使用 class 去定位文件如何加载我们定位到的文件呢?

3.1 WAR 包项目

因为 WAR 项目会被解压成为具体的文件(Tomcat),所以这里我们用传统的 File 描述一个对象,并读取即可。

public class ClassResource {
public static void main(String[] args) {
readFile("/1.txt");
} public static void readFile(String path) {
//1.定位资源
URL resource = ClassResource.class.getResource(path);
System.out.println("[getResource ] 读取文件:" + resource);
if (null == resource) {
System.out.println("找不到资源文件");
return;
} //2.映射资源
File file = new File(resource.getPath());
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
//3.读取资源
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}

执行结果:

可以看到能正常读取,这里不再叙述。

3.2 JAR 包项目

我们首先将上述方法打到 jar 包里面去运行,看一下效果

import com.yx.jtest.loadfile.ClassLoaderResource;
import com.yx.jtest.loadfile.ClassResource; public class Main { public static void main(String[] args) { System.out.println("#############打包后ClassResource开始读取文件############");
ClassResource.readFile("/1.txt");
}
}

执行结果:

可以看到,读取失败了:FileNotFoundException ,到这里,大家可以思考下,为什么文件读取不到了?

3.2.1 为什么路径是 jar: 开头

注意看红框部分输出的文件 URL,这个 URL 不再是以 file: 开头的了。这里先标记下,我们来跟踪下 classLoader.getResource() 的方法,来找到为什么是 jar: 开头。不感兴趣的同学可以跳过这部分

public class ClassLoader {
//... public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
}

熟悉的双亲委任模型,这里不多说,介绍下 ClassLoader 这个类和 Java 的类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

一种是启动类加载器 (Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且 全都继承自抽象类java.lang.ClassLoader。

摘自:《深入理解Java虚拟机-JVM高级特性与最佳实践》

其中启动类加载器和其他类加载器的关系,如下图所示:

到这里,我们能知道上述代码的 ClassLoader 实例的 parent 变量都是谁了,这里揭示下:

jar 启动调用的类加载器为 AppClassLoader,其 parentExtClassLoader,而 ExtClassLoader 的父加载器就是启动类加载器了

其中:

  • 启动类加载器 默认加载 <JAVA_HOME>/lib 目录下的能被虚拟机正确识别的类库
  • 扩展加载器 默认加载 <JAVA_HOME>/lib/ext 目录下的类库 可以看到,这两个都不是用来加载我们指定的文件的,加载 1.txt 只能是 AppClassLoader 的工作了。

因为父类加载器得到的 url 均为null,所以方法执行到 findResource(name) 这一行

AppClassLoader 本身没有这个方法的实现类,这里追踪到其父类 URLClassLoader 的实现

public class URLClassLoader {
//... public URL findResource(final String name) {
/*
* 忽略这个方法,可以看到是交个成员变量 ucp 去找资源了
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
//交给 ucp 寻找
return ucp.findResource(name, true);
}
}, acc); return url != null ? ucp.checkURL(url) : null;
}
}

这里的 ucp 变量,是个 URLClassPath 实例,继续往下追

public class URLClassPath {
//... public URL findResource(String var1, boolean var2) {
int[] var4 = this.getLookupCache(var1); URLClassPath.Loader var3; //找到对应的Loader
for (int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
//让Loader去找资源
URL var6 = var3.findResource(var1, var2);
if (var6 != null) {
//找到资源并返回
return var6;
}
} return null;
}
}

通过注释可以看到最终是通过 URLClassPath 的内部类 Loader 去定位的资源

这里介绍下 Loader 的两个实现类

  • JarLoader
  • FileLoader

到这里就不再往下追踪了,需要知道的是,打成Jar包后,文件的定位靠 JarLoader 来了

 private static class Loader implements Closeable {

    private final URL base;

    Loader(URL var1) {
this.base = var1;
}
} static class JarLoader extends Loader { private final URL csu; JarLoader(URL var1, URLStreamHandler var2, HashMap<String, URLClassPath.Loader> var3, AccessControlContext var4) throws IOException {
//这里设置 base url 的协议为 jar:
super(new URL("jar", "", -1, var1 + "!/", var2));
//..
} //1
URL findResource(String var1, boolean var2) {
//先获取resource, 找到 resource 获得其资源定位符 URL
Resource var3 = this.getResource(var1, var2); //返回 文件 url 给我们写的代码
//返回 文件 url 给我们写的代码
//返回 文件 url 给我们写的代码
return var3 != null ? var3.getURL() : null;
} //2
Resource getResource(String var1, boolean var2) {
//省略部分代码
//其他不看,看这里,checkResource后会返回resource
return this.checkResource(var1, var2, var3); } //3
Resource checkResource(final String var1, boolean var2, final JarEntry var3) {
final URL var4; //.. //获取初始化时候设置的 base url ,其协议为 jar,并重新封装目标 url,然后赋值给下面的 Resource 实例
var4 = new URL(this.getBaseURL(), ParseUtil.encodePath(var1, false)); //.. //返回资源
return new Resource() { public URL getURL() {
//上述的封装的目标 url
return var4;
}
// ..
};
}
}

所以我们获取到的资源定位就是以 jar: 开头的了

3.2.2 打 jar 包后,jar 包内资源为什么不能读取了

显而易见,对于 File 类来说,单个的 jar 文件,既是一个 File, 那么,再通过一个 File 去描述一个文件内部的 File 是不太合适的。

这有点像压缩文件一样:你不能直接操作压缩包内的文件。

那么,该如何快速方便地读取 jar 包内我们想要操作的文件(证书、固定配置)呢?

3.2.3 打 jar 包后,jar 包内资源该怎么读取

答案是,用流的形式,只要稍微改写就可以了,请看如下demo

public class ClassResource{

    //以流的形式读取文件
public static void readFileByStream(String path) {
System.out.println("[getResourceAsStream] 读取文件:" + path);
InputStream inputStream = null;
try {
//获得jar包内的文件的流
inputStream = ClassResource.class.getResourceAsStream(path);
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
} //输出文件内容
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
} } //jar 启动类
public class Main { public static void main(String[] args) {
System.out.println("#############打包后ClassResource开始读取文件############");
//注意这里的文件名,因为仍然是使用 Class.getResourceXxxx(),所以文件名解析路径方式仍然不变
//跟上述章节保持一致
ClassResource.readFileByStream("/1.txt");
}
}

输出结果:

可以看到,是能够正常读取 jar 内部文件的内容的

3.2.4 jar 包内资源的其他读取方法

也可以使用 JarFile 的形式去读取 jar 包内的资源,这种适合读取别的 jar 包内的资源,这里就不再介绍,感兴趣的同学可以自行百度。

3.3 SpringBoot JAR 包的文件加载方式

Spring boot 项目打包后不同于普通的 jar 包目录结构

执行原有jar读取方式代码

@SpringBootApplication
public class DemoApplication { public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
readFileByStream("/1.txt");
}
//以流的形式读取文件
public static void readFileByStream(String path) {
System.out.println("[getResourceAsStream] 读取文件:" + path);
InputStream inputStream = null;
try {
//获得jar包内的文件的流
inputStream = ClassResource.class.getResourceAsStream(path);
read(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
} //输出文件内容
private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}

输出结果:

可以看到,即使目录结构变了,Springboot jar 包也能正常读取到文件内容,这是因为,Spring boot 把如下两个目录添加到了 classPath 当中

  • BOOT-INF/classes
  • BOOT-INF/lib

Spring boot 额外提供了一种新的 jar 包内部的资源读取方式,即 ClassPathResource

@SpringBootApplication
public class DemoApplication { public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
//使用SpringBoot的方式读取资源文件,这里不再以 ‘/’ 开头,类似ClassLoader加载资源的name写法
ClassPathResource classPathResource = new ClassPathResource("1.txt");
try {
read(classPathResource.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
} private static void read(InputStream resource) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = resource.read()) != -1) {
baos.write(i);
}
System.out.println(new String(baos.toByteArray(), StandardCharsets.UTF_8));
}
}

执行结果:

四、总结

现今微服务大行其道,读取项目内的资源文件也常常在 SpringBoot jar中出现问题,这里使用 ClassPathResourceclass.getResourceAsStream()均可。

但是在企业提供高质量服务的目标下,应当把这些额外读取资源的需求,迁移到可配置化的环境当中,这样就能避免因改动配置引起的服务启停和中断。

本人才疏学浅,人微技轻,如有不妥之处,请留下宝贵批评指正。

jar\war\SpringBoot加载包内外资源的方式,告别FileNotFoundException吧的更多相关文章

  1. SpringBoot加载配置文件的几种方式

    首先回忆一下在没有使用SpringBoot之前也就是传统的spring项目中是如何读取配置文件,通过I/O流读取指定路径的配置文件,然后再去获取指定的配置信息. 传统项目读取配置方式 读取xml配置文 ...

  2. spring-boot 加载本地静态资源文件路径配置

    1.spring boot默认加载文件的路径是 /META-INF/resources/ /resources/ /static/ /public/ 这些目录下面, 当然我们也可以从spring bo ...

  3. jvm加载包名和类名相同的类的规则,以及如何加载包名和类名相同的类(转)

    jvm包括三种类加载器: 第一种:bootstrap classloader:加载java的核心类. 第二种:extension classloader:负责加载jre的扩展目录中的jar包. 第三种 ...

  4. Springboot 加载配置文件源码分析

    Springboot 加载配置文件源码分析 本文的分析是基于springboot 2.2.0.RELEASE. 本篇文章的相关源码位置:https://github.com/wbo112/blogde ...

  5. Microsoft Visual Studio 2008 未能正确加载包“Visual Web Developer HTML Source Editor Package” | “Visual Studio HTM Editor Package”

    在安装Microsoft Visual Studio 2008 后,如果Visual Studio 2008的语言版本与系统不一致时,比如:在Windows 7 English System 安装Vi ...

  6. Visual Studio 2008 Package Load Failure:未能正确加载包“Microsoft.VisualStudio.Xaml”

    在安装好Visual Studio 2008后,启动Visual Studio 2008 发现如下提示: 包加载失败 未能正确加载包“Microsoft.VisualStudio.Xaml”( GUI ...

  7. 未能加载包“Microsoft SQL Server Data Tools”

    直接在vs2013里的App_Data目录创建数据库,在服务器资源管理器中查看时报错: 未能加载包“Microsoft SQL Server Data Tools” 英文: The 'Microsof ...

  8. 使用NuGet加载包,发现加载的dll都是最新版,原来少加了参数[-Version]

    使用NuGet获取AutoMapper 发现无法正确加载包,项目版本是3.5,获取的dll版本较高,查资料发现可以通过 “-Version” 指定加载包版本 http://www.mamicode.c ...

  9. 开启Microsoft SQL Management时,如果出现"未能加载包

    Ms Sql server 2005在开启Microsoft SQL Management时,如果出现"未能加载包“Microsoft SQL Management Studio Packa ...

随机推荐

  1. CoSky 高性能 服务注册/发现 & 配置中心

    CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...

  2. BASE理论之思考

    一.什么是BASE理论? BASE理论是对CAP中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性. BASE理论 ...

  3. 点云配准的端到端深度神经网络:ICCV2019论文解读

    点云配准的端到端深度神经网络:ICCV2019论文解读 DeepVCP: An End-to-End Deep Neural Network for Point Cloud Registration ...

  4. Structured Streaming编程 Programming Guide

    Structured Streaming编程 Programming Guide Overview Quick Example Programming Model Basic Concepts Han ...

  5. Ascend Pytorch算子适配层开发

    Ascend Pytorch算子适配层开发 适配方法 找到和PyTorch算子功能对应的NPU TBE算子,根据算子功能计算出输出Tensor的size,再根据TBE算子原型构造对应的input/ou ...

  6. gpgpu-sim卡分配程序设计实例分析

    gpgpu-sim卡分配程序设计实例分析 运行代码地址:https://github.com/gpgpu-sim/gpgpu-sim_distribution 一.概述 此文件包含有关安装.生成和运行 ...

  7. 菜鸟刷题路:剑指 Offer 05. 替换空格

    剑指 Offer 05. 替换空格 class Solution { public String replaceSpace(String s) { StringBuilder str = new St ...

  8. Qt中的多线程与线程池浅析+实例

    1. Qt中的多线程与线程池 今天学习了Qt中的多线程和线程池,特写这篇博客来记录一下 2. 多线程 2.1 线程类 QThread Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一 ...

  9. python通过字典实现购物车案例-用户端

    import os dict01 = { 'iphone' : { '5999' : { '总部位于美国' : '价格相对较贵', }, }, 'wahaha' : { '15' : { '总部位于中 ...

  10. 从架构师角度谈谈mybatis-plus可能存在的问题

    存在这么一个情况:对于缺营养的人来说,医生更倾向于建议他选择纯牛奶,而不是有机奶(因其有添加剂).然而,大部分人却更加倾向于选择有机奶, 因其口感不错,因此,对于选择纯牛奶还是有机奶,这是个博弈问题. ...