标签: SpringBoot

springboot中SPI机制

一、从java类加载机制说起

  • java中的类加载器负载加载来自文件系统、网络或者其他来源的类文件。jvm的类加载器默认使用的是双亲委派模式。三种默认的类加载器Bootstrap ClassLoader、Extension ClassLoader和System ClassLoader(Application ClassLoader)每一个中类加载器都确定了从哪一些位置加载文件。于此同时我们也可以通过继承java.lang.classloader实现自己的类加载器。

Bootstrap ClassLoader:负责加载JDK自带的rt.jar包中的类文件,是所有类加载的父类
Extension ClassLoader:负责加载java的扩展类库从jre/lib/ect目录或者java.ext.dirs系统属性指定的目录下加载类,是System ClassLoader的父类加载器
System ClassLoader:负责从classpath环境变量中加载类文件

%title插图%num

java类加载结构

1、双亲委派模型

  • 原理:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此*终加载任务都会传递到*顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

具体:根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递直Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止。

  • 采用双亲委派模式可以保证类型加载的安全性,不管是哪个加载器加载这个类,*终都是委托给顶层的BootstrapClassLoader来加载的,只有父类无法加载自己猜尝试加载,这样就可以保证任何的类加载器*终得到的都是同样一个Object对象。
  1. protected Class<?> loadClass(String name, boolean resolve) {
  2. synchronized (getClassLoadingLock(name)) {
  3. // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回
  4. Class<?> c = findLoadedClass(name);
  5. if (c == null) {
  6. try {
  7. // 遵循双亲委派的模型,首先会通过递归从父加载器开始找,
  8. // 直到父类加载器是BootstrapClassLoader为止
  9. if (parent != null) {
  10. c = parent.loadClass(name, false);
  11. } else {
  12. c = findBootstrapClassOrNull(name);
  13. }
  14. } catch (ClassNotFoundException e) {}
  15. if (c == null) {
  16. // 如果还找不到,尝试通过findClass方法去寻找
  17. // findClass是留给开发者自己实现的,也就是说
  18. // 自定义类加载器时,重写此方法即可
  19. c = findClass(name);
  20. }
  21. }
  22. if (resolve) {
  23. resolveClass(c);
  24. }
  25. return c;
  26. }
  27. }

2.双亲委派模型缺陷

  • 在双亲委派模型中,子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载器问题。
  • 案例:Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是*顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题

3.使用线程上下文类加载器(ContextClassLoader)加载

  • 如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。
  • 通常我们可以通过Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。

4、使用类加载器加载资源文件,比如jar包

类加载器除了加载class外,还有一个非常重要功能,就是加载资源,它可以从jar包中读取任何资源文件,比如,ClassLoader.getResources(String name)方法就是用于读取jar包中的资源文件

  1. //获取资源的方法
  2. public Enumeration<URL> getResources(String name) throws IOException {
  3. Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
  4. if (parent != null) {
  5. tmp[0] = parent.getResources(name);
  6. } else {
  7. tmp[0] = getBootstrapResources(name);
  8. }
  9. tmp[1] = findResources(name);
  10. return new CompoundEnumeration<>(tmp);
  11. }

它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到BootstrapClassLoader,*后才轮到自己查找。而不同的类加载器负责扫描不同路径下的jar包,就如同加载class一样,*后会扫描所有的jar包,找到符合条件的资源文件。

  1. // 使用线程上下文类加载器加载资源
  2. public static void main(String[] args) throws Exception{
  3. // Array.class的完整路径
  4. String name = “java/sql/Array.class”;
  5. Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
  6. while (urls.hasMoreElements()) {
  7. URL url = urls.nextElement();
  8. System.out.println(url.toString());
  9. }
  10. }

二、spring中SPI机制实现

1.SPI机制

(1)SPI思想

  • SPI的全名为Service Provider Interface.这个是针对厂商或者插件的。
  • SPI的思想:系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制

(2)SPI约定

  • 当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。

2、SPI使用案例

  • common-logging apache*早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。

3、springboot中的类SPI扩展机制

  • 在springboot的自动装配过程中,*终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
  1. public static final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”;
  2. // spring.factories文件的格式为:key=value1,value2,value3
  3. // 从所有的jar包中找到META-INF/spring.factories文件
  4. // 然后从文件中解析出key=factoryClass类名称的所有value值
  5. public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
  6. String factoryClassName = factoryClass.getName();
  7. // 取得资源文件的URL
  8. Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
  9. List<String> result = new ArrayList<String>();
  10. // 遍历所有的URL
  11. while (urls.hasMoreElements()) {
  12. URL url = urls.nextElement();
  13. // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
  14. Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
  15. String factoryClassNames = properties.getProperty(factoryClassName);
  16. // 组装数据,并返回
  17. result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
  18. }
  19. return result;
  20. }

转载自:https://www.jianshu.com/p/0d196ad23915

springboot以FTP方式上传文件到远程服务器

此处远程服务器是ubuntu,关于ftpserver的配置请参考该文https://blog.csdn.net/sunxiaoju/article/details/85224602,在此处就不再赘述。该篇文章主要针对如何实现代码来进行代码干货的分享。

本小编用的前台为layui框架。话不多说直接上干货,代码没什么好解释的,直接贴出前后台代码以供大家分享使用。

一、html代码

<div class=”layui-form-item”>
<label class=”layui-form-label”>上传附件:</label>
<div class=”layui-input-block doc-litpic”>
<button type=”button” name=”avatar” class=”layui-btn layui-btn-sm” data-url=”/admin/archives/upload.html” id=”larry-litpic”><i class=”layui-icon”></i>本地上传</button>
<a id=”upload-filename-display” style=”color: blue” href=””></a>
<div class=”larryms-img-view”>
</div>
</div>
</div>
二、js代码

upload.render({
accept: ‘file’,
elem: ‘#larry-litpic’,
url: interface_cms_article_upload,
field: ‘fileNames’,
done : function (res, index, upload) {
if(res.code != 200){
layer.open({
icon : 2,
skin : “layui-layer-molv”,
content : res.msg
});
}else{
layer.open({
icon : 1,
skin : “layui-layer-molv”,
content : res.msg
});
$(‘#upload-filename-display’).text(res.filename);
$(“input[name=’fileId’]”).val(res.filename);
}
},
error : function (res) {

}
});
三、后台controller:

@RequestMapping(value = “/upload”)
@ApiOperation(value = “本地文件上传”,notes =”本地文件上传” )
public Map uploadfunction(HttpServletRequest request, HttpServletResponse response){
//创建文件对象并获取请求中的文件对象
MultipartFile file = null;
Map resultData = new HashMap();

try{
MultipartHttpServletRequest mRequest = (MultipartHttpServletRequest) request;
file = mRequest.getFile(“fileNames”);

//判断上传非空
if(null == file) {
resultData.put(“code”,0);
resultData.put(“msg”,”上传文件失败”);
resultData.put(“filename”,file.getOriginalFilename());
return resultData;
}
//上传需要导入数据的文件
//用来检测程序运行时间
long startTime=System.currentTimeMillis();
System.out.println(“上传的文件名为:”+file.getOriginalFilename());
String fileName = file.getOriginalFilename();

InputStream inputStream = file.getInputStream();

String hostName = uploadUtil.getHostname();
String username = uploadUtil.getUsername();
String password = uploadUtil.getPassword();
String targetPath = uploadUtil.getTargetPath();
String suffix = cmsArticleService.getSuffix(fileName);
fileName = cmsArticleService.upload(hostName,username,password,targetPath,suffix,inputStream);
//计算上传时间
long endTime=System.currentTimeMillis();
String uploadTime = String.valueOf(endTime-startTime);
System.out.println(“上传所用时间:”+uploadTime+”ms”);

resultData.put(“code”,200);
resultData.put(“msg”,”上传文件成功”);
resultData.put(“filename”,fileName);
return resultData;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
四、后台service上传至远程服务器

//FTP文件上传
public static String upload(String hostname,String username,String password,
String targetPath,String suffix,InputStream inputStream) throws SocketException, IOException {
//实例化ftpClient
FTPClient ftpClient = new FTPClient();
//设置登陆超时时间,默认是20s
ftpClient.setDataTimeout(12000);
//1.连接服务器
ftpClient.connect(hostname,21);
//2.登录(指定用户名和密码)
boolean b = ftpClient.login(username,password);
if(!b) {
System.out.println(“登陸超時”);
if (ftpClient.isConnected()) {
// 断开连接
ftpClient.disconnect();
}
}
// 设置字符编码
ftpClient.setControlEncoding(“UTF-8″);
//基本路径,一定存在
String basePath=”/”;
String[] pathArray = targetPath.split(“/”);
for(String path:pathArray){
basePath+=path+”/”;
//3.指定目录 返回布尔类型 true表示该目录存在
boolean dirExsists = ftpClient.changeWorkingDirectory(basePath);
//4.如果指定的目录不存在,则创建目录
if(!dirExsists){
//此方式,每次,只能创建一级目录
boolean flag=ftpClient.makeDirectory(basePath);
if (flag){
System.out.println(“创建成功!”);
}
}
}
//重新指定上传文件的路径
ftpClient.changeWorkingDirectory(targetPath);
//5.设置上传文件的方式
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
//使用uuid,保存文件名唯一性
String uuid= UUID.randomUUID().toString();
/**
* 6.执行上传
* remote 上传服务后,文件的名称
* local 文件输入流
* 上传文件时,如果已经存在同名文件,会被覆盖
*/
boolean uploadFlag = ftpClient.storeFile(uuid+suffix,inputStream);
if(uploadFlag)
System.out.println(“上传成功!”);
return uuid+suffix;
}
五、获取yml配置的工具类

@Data
@Component
public class UploadUtil {
@Value(“${upload.hostname}”)
private String hostname;
@Value(“${upload.username}”)
private String username;
@Value(“${upload.password}”)
private String password;
@Value(“${upload.targetPath}”)
private String targetPath;
}

Android OKHttp 获取服务器的 Json 数据,没有双引号?

我服务器使用 SpringBoot 写的,如下:

    @GetMapping(value = "/GetImageByUser",produces = "application/json;charset=UTF-8")
    @ApiOperation("获取用户图片,每个分类前 100 张")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "userId", value = "userId", dataType = "String", paramType = "query")
    }
    )
    public Result getImageByUser(String userId) {
        return imageService.getImageByUser(userId);
    }

返回值 Result 对象如下:

public class Result {
    private int code;
    private boolean message;
    private Object detail;
    ...省略 setter 和 getter 方法
}

然后在浏览器访问这个接口的时候,响应结果是带双引号的:

{"code":200,"message":true,"detail":[{"category":{"objectId":"0c0a0ddaa1b84ce286be4343cd9d3438","categoryId":"c5341c1d8ac547589899514516d0768a","categoryName":"WeiXin","categoryUser":"84f6e0d8d94348ccab981e364661a419","isPrivate":-1,"categoryStatus":1,"createTime":3},"images":[{"objectId":"f6d97d24556348a4ac342cb321397718","imageId":"e50ba0a75359432f89390d85daefb10a","sha1":"c3063275501e0f8ef87703473c7263168771f21b","fileCategory":"c5341c1d8ac5475898
省略...

然后使用 Swagger 做调试的时候也是同样的效果。

但是在 Android 端使用 OKHttp 访问接口的时候,json 的 key 和 value 都不带双引号:

[{category={objectId=0c0a0ddaa1b84ce286be4343cd9d3438, categoryId=c5341c1d8ac547589899514516d0768a, categoryName=WeiXin, categoryUser=84f6e0d8d94348ccab981e364661a419, isPrivate=-1.0, categoryStatus=1.0, createTime=3.0}, images=[{objectId=f6d97d24556348a4ac342cb321397718, imageId=e50ba0a75359432f89390d85daefb10a, sha1=c3063275501e0f8ef87703473c7263168771f21b, 
省略...

因为 json 中有时候会含有网址,所以在解析 JSON 的时候就会出错。

我感觉问题应该是出在 OKHttp 上,因为在浏览器是正常的(带双引号),使用 swagger 调试接口的时候也是正常的

请问这个问题该如何解决啊?如何让 key 和 value 中的字符串都是带双引号的呢?

14 条回复
grantonzhuang
    1

grantonzhuang   73 天前

你这打印的不是 json,是 toString 方法打印出来的
w292614191
    2

w292614191   73 天前

OKHttp 可不会出这种低级错误。
qwerthhusn
    3

qwerthhusn   73 天前

目测 lz 新人,这个 JSON 已经反序列化成对象了,打印的是 toString 又不是 json
beichenhpy
    4

beichenhpy   73 天前

访问接口这个打出来的已经是反序列化的对象了,自然没有引号
Helsing
    5

Helsing   73 天前 via iPhone

找一个可以打印 json 的库,xlog 、logger 之类的
hongch
    6

hongch   73 天前

response.body.string 就是带双引号的
kingfalse
    7

kingfalse   73 天前 via Android

okhttp 直呼内行并表示你还是换个 HTTP 请求库吧
yinzhili
    8

yinzhili   73 天前

楼主需要补一补基础知识了
Paaranoia
    9

Paaranoia   73 天前

建议看看 okhttp 的文档
scxiazi
    10

scxiazi   73 天前

这打印的是对象

amok
    11

amok   73 天前

应该只是打印的问题,看看打印的代码是怎么写的,是否打印的 toString ?这里应该用拦截器打印 http 报文。
Vegetable
    12

Vegetable   73 天前   ❤️ 2

还建议看 okhttp 文档呢?这显然是需要重学 java 啊
lychs1998
    13

lychs1998   73 天前

这是 toString 出来的结果吧。看看有没有实体类自己输出 jsonStr 的方法。
unco020511
    14

unco020511   73 天前

你这是对象的 toSting(),不是 json
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速