猴哥一一

猴哥一一 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 segmentfault.com/u/hougeyiyi 编辑
编辑

Java开发工程师

个人动态

猴哥一一 发布了文章 · 6月26日

Why Spring ???

本文是一次 spring 官网的 why spring 翻译记录(末尾有正经的彩蛋),试图培养一下阅读官方文档的习惯。

不知这个场景你是否熟悉?

你丢了一个问题到 xx技术交流群:请问大佬......怎么解决?

某大佬高冷的丢出两个字:看官方文档。

此刻,除了感受到社会的”冷漠“,你有没有思考过这样一个问题。

你一般通过什么途径来学习的?通过视频,博客?

你学的东西,原本的样子是什么?有没有看过官方对它的解读?万一别人的解读是错误的怎么办?

我一度没有读官网文档的习惯(一般都以英文读不懂为接口),感觉是时候学习一下了。

最近打算重新了解一下这个又爱又恨的 Spring,从简单的来,先看一下官网。

在官网首页注意到了 why spring 栏目,行,就它了。

整个过程借助翻译工具翻译,并记录下来,混个脸熟,毕竟以后有更多的文档要阅读呢。

于是就有了本篇,希望这是个人学习的一个新起点,期待一些习惯的养成。

本文仅献给使用 Spring 很久了,却没看过 Spring 官方资料的你(当然看过的大佬们也欢迎。。。)

进入正题,看官网怎么说 Spring。

Spring.io

行文按照 why spring 内容从上到下,一段原文,一段翻译进行。

Why Spring?

为什么是 Spring 呢?(这句看似简单的发问透出了 Spring 无数的骄傲呀)

Spring makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simplicity, and productivity has made it the world's most popular Java framework.

Spring 使每个人都可以更快、更简单、更安全地进行 Java 编程。 Spring 对速度,简单性和生产力的关注使其成为世界上最受欢迎的 Java 框架。

“We use a lot of the tools that come with the Spring framework and reap the benefits of having a lot of the out of the box solutions, and not having to worry about writing a ton of additional code—so that really saves us some time and energy.”

-- SEAN GRAHAM, APPLICATION TRANSFORMATION LEAD, DICK’S SPORTING GOODS

“我们使用了 Spring 框架随附的许多工具,并获得了许多现成的解决方案的好处,而不必担心编写大量额外的代码-这样确实为我们节省了一些时间和能量。”

--某某大佬

Spring is everywhere

Spring 无处不在(先套近乎)

Spring’s flexible libraries are trusted by developers all over the world. Spring delivers delightful experiences to millions of end-users every day—whether that’s streaming TV, connected cars, online shopping, or countless other innovative solutions. Spring also has contributions from all the big names in tech, including Alibaba, Amazon, Google, Microsoft, and more.

Spring 灵活的库受到全世界开发人员的信任。Spring 每天都为数以百万计的终端用户提供令人愉快的体验,无论是流媒体电视、联网汽车、在线购物,还是其他无数的创新解决方案。包括阿里巴巴、亚马逊、谷歌、微软等在内的科技巨头也为 Spring 做出了贡献。

Spring is flexible

Spring 是灵活的(胖子 🤣)

Spring’s flexible and comprehensive set of extensions and third-party libraries let developers build almost any application imaginable. At its core, Spring Framework’s Inversion of Control (IoC) and Dependency Injection (DI) features provide the foundation for a wide-ranging set of features and functionality. Whether you’re building secure, reactive, cloud-based microservices for the web, or complex streaming data flows for the enterprise, Spring has the tools to help.

Spring 灵活而全面的扩展集和第三方库让开发人员可以构建几乎所有可以想象到的应用程序。在其核心,Spring 框架的控制反转(IoC)和依赖注入(DI)特性为广泛的特性和功能集提供了基础。无论您是在构建安全的、反应式的、基于云的微服务 web 应用,还是为企业构建复杂的流数据流,Spring 都有工具帮得上忙。

Spring is productive

Spring 就是生产力的象征!(多吊)

Spring Boot transforms how you approach Java programming tasks, radically streamlining your experience. Spring Boot combines necessities such as an application context and an auto-configured, embedded web server to make microservice development a cinch. To go even faster, you can combine Spring Boot with Spring Cloud’s rich set of supporting libraries, servers, patterns, and templates, to safely deploy entire microservices-based architectures into the cloud, in record time.

Spring Boot 改变了您处理 Java 编程任务的方式,从根本上简化了您的体验。Spring Boot 将应用程序上下文和自动配置的嵌入式 web 服务器等必需品组合在一起,从而使微服务开发变得轻松。为了更快,您可以将 Spring Boot 与 Spring Cloud 的丰富的支持库、服务、模式和模板组合在一起,以在极短的时间内安全地将整个基于微服务的架构部署到云中。

Spring is fast

Spring 就是快

Our engineers care deeply about performance. With Spring, you’ll notice fast startup, fast shutdown, and optimized execution, by default. Increasingly, Spring projects also support the reactive (nonblocking) programming model for even greater efficiency. Developer productivity is Spring’s superpower. Spring Boot helps developers build applications with ease and with far less toil than other competing paradigms. Embedded web servers, auto-configuration, and “fat jars” help you get started quickly, and innovations like LiveReload in Spring DevTools mean developers can iterate faster than ever before. You can even start a new Spring project in seconds, with the Spring Initializr at start.spring.io.

我们的工程师非常关注性能。使用 Spring,默认情况下,您会注意到快速启动、快速关闭和优化的执行。其实渐渐的,Spring 项目也越来越支持响应式(非阻塞)编程模型,以获得更高的效率。开发人员的生产力是 Spring 的超级能力。Spring Boot 帮助开发人员轻松地构建应用程序,而且比其他同类竞品更简单。嵌入式 web 服务器、自动配置和 fat jars 特性可以帮助您快速入门,Spring DevTools 中的 LiveReload 等创新意味着开发人员可以以前所未有的速度迭代开发。

Spring is secure

Spring 是安全的

Spring has a proven track record of dealing with security issues quickly and responsibly. The Spring committers work with security professionals to patch and test any reported vulnerabilities. Third-party dependencies are also monitored closely, and regular updates are issued to help keep your data and applications as safe as possible. In addition, Spring Security makes it easier for you to integrate with industry-standard security schemes and deliver trustworthy solutions that are secure by default.

Spring 在快速、负责地处理安全问题方面有良好的记录。Spring 的 committers 与安全专业人员一起对任何报告的漏洞进行补丁和测试。第三方依赖也被密切关注,并定期发布更新,以帮助您的数据和应用程序尽可能安全。此外,Spring Security 使您更容易与行业标准的安全方案集成,并默认提供安全可靠的解决方案。

Spring is supportive

Spring 是有支撑的(包你学习过程不孤单,头大的不止你一个 😁 )

The Spring community is enormous, global, diverse, and spans folks of all ages and capabilities, from complete beginners to seasoned pros. No matter where you are on your journey, you can find the support and resources you need to get you to the next level: quickstarts, guides & tutorials, videos, meetups, support, or even formal training and certification.

Spring 社区是巨大的、全球性的、多样化的,涵盖了各个年龄和能力级别的人,从小白初学者到经验丰富的专业人士。无论你处在其中哪个阶段,你都可以找到你需要的支持和资源,让你进入下一个层次:快速入门,指南和教程,视频,聚会,支持,甚至正式的培训和认证。

What can Spring do?

Spring 能干啥呢?

  • Microservices

    Quickly deliver production‑grade features with independently evolvable microservices.

    通过可独立开发的微服务快速交付生产级功能。

  • Reactive
    Spring's asynchronous, nonblocking architecture means you can get more from your computing resources.

    Spring 的异步,非阻塞架构意味着您可以从计算资源中获得更多收益。

  • Cloud
    Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.

    您的代码,任何云-我们已为您覆盖。无论您使用什么平台,都可以连接并扩展您的服务(牛逼哄哄)。

  • Web Applications
    Frameworks for fast, secure, and responsive web applications connected to any data store.

    连接到任何数据存储的快速,安全和响应式Web应用程序的框架。

  • Serverless
    The ultimate flexibility. Scale up on demand and scale to zero when there’s no demand.

    极致的灵活性。按需扩展并在没有需求时扩展为零。

  • Event Driven
    Integrate with your enterprise. React to business events. Act on your streaming data in realtime.

    与您的企业集成。对业务事件做出反应。实时处理您的流数据。

  • Batch
    Automated tasks. Offline processing of data at a time to suit you.

    自动化任务。一次适合您的离线数据处理。

不正经的彩蛋

why-spring

在 why spring 官方页面的右上角注意到了这么一张图片,可以看到 Spring 周围链接了各种线,寓意着 Spring 的勃勃野心,当然人家也做到了,确实在服务(侵入)到了各个领域。

不,这不是我要的彩蛋!

注意到没?图片里又是飞机,又是汽车的,这不就是我们业界经常吐槽的写照嘛:

面试造飞机,工作拧螺丝 ???

Spring 你够了!!! 😤

完结,撒花。

祝你学习 Spring 的过程,愉快而有趣(骗谁呢又)!

下回再见!

查看原文

赞 0 收藏 0 评论 0

猴哥一一 发布了文章 · 6月20日

[安利] 可能会让你爱上书写的工具组合!

吐血推荐一款可能会让你爱上书写的工具组合 :Typora + Snipaste !

温馨警告:内附精心制作 GIF 使用图

献给你的写作工具组合:

  • Typora 📒

一款支持实时预览的 Markdown 文本编辑器

  • Snipaste 🎨

一个简单但强大的截图工具

Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,国内很多内容分享网站都支持 Markdown 格式的编辑器。

看到这里请不要被「标记」、「语言」所迷惑,Markdown 的语法十分简单。常用的标记符号也不超过十个,这种相对于更为复杂的HTML 标记语言来说,Markdown 可谓是十分轻量的,学习成本也不需要太多,且一旦熟悉这种语法规则,会有一劳永逸的效果。

建议你了解下 Markdown ,好处多多。

其实初次接触 Markdown 语法时,并没有太大兴趣。虽然看起来挺富有极客气息的,但是第一尝试使用并不是不太顺手,特别是表格的语法,写起来简直要吐了。感觉又是定义了一套自己的规则,让别人去使用,没劲。

直到第一次遇见了 Typora :看到同事用 Typora 写的接口文档,让我改变了对 Markdown 的看法,原来可以显示的这么好看。

Markdown 结合正确的编辑器,真的是简单却不失优雅,真 TM 香呀!🤞

Typora 是我们的主角,没有它,我依然不会喜欢 Markdown 语法。

到现在使用 Typora 也将近有一年的时间了,它已经成为我高频使用的编辑器了。

我会拿它做什么呢?

  • 作为日常工作开发过程记录本,记录一些关键信息,步骤,因为写起来实在是赏心悦目
  • 做为最近的总结分享编辑器的,写完之后再同步到写作平台上去
  • 我还用 Markdown 写了篇简历,看起来还不丑,对没错,可以用来写简历

Typora

Typora 是一款让 Markdown 写作更简单,免费极简的编辑器,支持 OS X、Windows、Linux 三个平台哦。

就像官网上面描述的那样:

  • 写作免打扰
  • 实时预览
  • 所见即所得

下面是用官网的一个微视频做的 gif,这其实就是 Typora 书写时的状态。

一起都是这么自然,沉浸式,所见即所得,关注内容本身,而不是样式,简单却不简陋。

爱上它只需要一眼。

所见即所得

Typora 改善 Markdown 易用的地方

如果说 Markdown 是少数派的技术人员所吹捧的一种标记语言,那么 Typora 绝对是让它面向大众的一个利器!

这里主要介绍下 Typora 提升我们使用 Markdown 易用性的地方

标题

Markdown 本身语法是 # 一级标题,# 的个数代表着标题的级别,一个 # 就代表一级标题。

Typora 里,提供了快捷键,很方便使用。

按住 Ctrl 键盘,然后按 1-6 的数字键,即可把文本转成对应级别的标题。

看下效果。

image-20200619192253673

还可以 ctrl + '-/=',放大或缩小

标题快捷键

加粗

Markdown 本身语法是 ** 文本 **,用起来稍有麻烦,找到文本起始点,分别键入 **。

Typora 提供了快捷方式Ctrl + b,一键变粗,变细,来回切换,爽!

(由于我之前没使用快捷键,手敲了很多 ** 。。。)

加粗演示

表格

Markdown 本身语法长这样,用起来足以搞死强迫症。

| 表头1 | 表头2 | 表头3 |
| ----- | ----- | ----- |
|       |       |       |
|       |       |       |

效果如下(默默的吹一下 Typora 的表格看起来真舒服)

image-20200619193658573

Typora 提供了快捷键,Ctrl + t,一键唤出表格编辑(右键->插入->表格也能达到这个效果),然后还可以动态的对表格进行增删,非常方便,好用。

动态调整

插入图片

Markdown 语法![图片描述](图片路径),

Typora 里还支持从粘贴板粘贴图片。

比如从截图工具复制的,或者右键复制的浏览器的网络图片,都可以直接快捷粘贴。

从粘贴板插入图片

其他元素的使用

代码块

Markdown 本身语法是 ``java + 换行`,这个元素使用 Markdown 语法足以。

​```java
# 这是一段代码块的效果,```后面的java代表代码块的语言版本,可以自己调整,比如 shell,javascript ...
​```

区块

Markdown 语法> 文本

区块的效果就是这样,常常用来引用。

鲁迅先生说过,世界上本没有路,走的人多了便成了路。

分割线

Markdown 语法---,效果如下。


行了,以分隔线结束 Typora 的日常使用介绍。

不再过多篇幅描述 Markdown 语法了,重点关注上面几个 Typora 里给咱们提供的快捷键。

如果把 Typora 比作是一个 ADC,那么 Snipaste 就是那个出色的辅助。

Snipaste

先说截图这事本身:

有微信,QQ,甚至钉钉了,为什么还要安装一个额外的截图软件呢?

怎么说呢,用这些工具提供的截图功能当然也没问题。

用这些软件附带的截图功能的前提就是:这些软件要先启动!!!

有时候你需要的只是截图功能,而不想把整个软件启动,会不太方便。

比如周末在家休闲的时候,我想截个图,你还让我启动钉钉 ???还让不让人玩了。

索性,还是自己装一个小巧实用的截图软件吧。

Snipaste 是一款简单但强大的截图工具,安装完设置成开机启动即可随时使用。

截图这事很简单,没必要说太多,按照使用习惯,配置下快捷键就行。

快捷键设置

比如我的快捷键是这样:

常用的就是 截屏,鼠标穿透

image-20200619202917137

贴图

关于这款截图软件,说下比较好玩的功能贴图:就是可以把你的截图"钉"在屏幕上,随意拖动,还可以控制大小,以及图片透明度。

我用贴图主要有以下场景:

  • 把重要的东西临时钉到屏幕的某个地方,方便查看,比如我会把开发计划钉到屏幕,边思考如何实现,边看计划内容。
  • 有时候需要对比一些内容的差异,很方便的就贴一下。
  • 有时候想把多个在不同区域的内容组合到同一张图片中保存下来,把几个贴图一组合就 OK。
我最近开发的过程中,经常会把开发计划钉到屏幕上,缩小到很小,透明度10%,放到某个某个不起眼的角落,随时想看的时候,再放大它

一张图看明白贴图怎么用

xxx

根据我自身的快捷键解读下上图的操作,

  • alt + s:开始截图,圈选要截图的区域
  • ctrl + t:将图片钉到屏幕(图片中是点击“钉”图标完成的贴图),拖放到合适的位置
  • 滚动鼠标滚轮:对贴图进行缩放
  • ctrl + 鼠标滚轮:对贴图透明度进行调整

鼠标穿透

鼠标穿透也是个有用的功能,跟贴图搭配使用。

鼠标穿透就是:图片虽然固定在桌面,但是不影响咱们对图片下面软件的操作(一般要降低贴图的透明度,不然咱也看不见下面操作的啥呀)。

看看鼠标穿透的效果

img

体验一下吧,这个截图工具用起来非常爽。

想详细了解更多玩法,可以去官网看看文档。


To Be Better:给 Typora 一个图床

Markdown 都用上了,如果不搞个图床的话,像缺了灵魂一样。

说实话,在解决了图床问题之后,我才真正用起来 Typora ,并完全爱上了它。

这里简单介绍一下 Typora 使用免费图床的方法,也是在最近版本 Typora 才支持的功能。

简单说下图床原理:

  • 这里咱们是借助 GitHub,Gitee 的仓库功能来实现免费图床,这里我推荐 Gitee 吧,毕竟属于国内的网站,网速要快一些。
  • 借助 PicGo 这款免费工具实现图片上传到 git 仓库。
  • Typora 使用 PicGo 的上传能力上传图片,然后把本地图片替换成网络图片

设置步骤

  • Typora、PicGo、node.js 软件安装
  • 注册 Gitee 账号, 创建公开的仓库
  • 获取 Gitee 私人令牌
  • 配置 PicGo

    • 安装 github-plus 插件
    • 设置图床信息
  • 配置 Typora 图床

关键步骤说明

软件安装就不说了,简单贴下关键步骤的参考。

获取私人令牌

私人令牌是用来调用 gitee 的图片上传 api 的,注意保密哦。

路径:设置 --> 私人令牌

img

安装 github-plus 插件

在 PicGo 中安装插件,借助插件能力来上传图片。

img

设置图床信息

这里是配置一下咱们想要把图片上传到什么地方去。

image-20200619220352510

关于 path 参数再说明一下,免得折腾。

image-20200619220543249

检查一下上传是否正常

在这里上传个图片,看是否能成功。

image-20200619221857989

Typora 的图床设置

路径:文件 --> 偏好设置 --> 图像

img

然后你从粘贴板粘贴图片到编辑区,就会自动上传,并替换成网络图片地址了。

就像上面介绍插入图片这张图一样

从粘贴板插入图片


完结,撒花!

你学会了吗?

希望 Typora 这款 Markdown 编辑器能给你带来使用上的喜悦,甚至让你爱上书写,爱上记录。

查看原文

赞 0 收藏 0 评论 0

猴哥一一 发布了文章 · 6月13日

你了解SpringBoot java -jar 的启动原理吗?

电话面试中,面试官问了一个问题:你知道 java -jar 启动 Spring Boot 项目,和传统的 jar 有什么不一样的吗?

问题大概是这样,当时不太清楚怎么回答,面试结束之后知道面试估计是挂了,请教了一下面试官这个问题应该从哪方面去考虑呢?

大概记得面试官说,... 自定义类加载器知道吗? ...(中间一些内容就没听进去了)

我:原来是从这方面去考虑呀,感谢面试官的指点!

事后赶紧学了学,也走读了下启动过程的源码,终于知道他说的自定义类加载器了,也就知道他问这个问题的目的所在了。

凡是你接触过一点点 Spring Boot 项目,你一定知道通过 java -jar xxx.jar 命令便能把一个 Spring Boot 服务启动起来。(如果你还没接触过,这里的内容可以日后再看,先轻微了解一下 Spring Boot 项目的玩法)

一个看似简陋的 java -jar 究竟干了什么,就把咱们手写的应用(咱们的项目可能叫 XXXApplication.java)启动了呢?

这就是本文的目的,解读一下 java -jar 都做了什么。

至少面试的时候能搭上话,能说两句,不会像我一样只能哦哦哦的。。。

先有个概览

了解一个技术点,直接扎到源码堆里,云里雾里,很难受,容易让人望而生畏。

这时候可以先从整体或者非源码的角度了解一下它的运作机制,心里有个底,如果再感兴趣,就可以找一些细节,慢慢击破,可能效果更好,更能让人坚持下去。

这也是我后面准备学习源码的思路,就写一下。

虽然也是这样劝自己,可是还是看不懂,尴尬了,哈哈哈...

咱们就先拿这个java -jar xxx.jar来说:

Spring Boot 在可执行 Fat jar 包中定义了自己的一套规则,比如第三方依赖 jar 包在 /lib目录下,jar 包的 URL 路径使用自定义的规则并且这个规则需要使用 org.springframework.boot.loader.jar.Handler 处理器处理。

Fat jar 的 Main-Class 使用 org.springframework.boot.loader.JarLauncher,也就是 执行java -jar xxx.jar首先会触发 JarLauncher的main方法的执行,而不是咱们的应用的xxx.xxx.xxx.XXXApplication

不过不用急,JarLauncher#main 会执行一些逻辑,做一些物料准备,最终会触发咱们的 XXXApplication#main 启动应用。

先看个启动过程概览,日后研究不会慌!

还不会画时序图,不搞个呢又感觉少了些直观的东西,就勉强搞了个,这张图的主要目的是提供启动过程的调用关系。

JarLauncher启动流程

怕时序图表达不够完善,再把简要代码贴一下,哈哈。。。

JarLauncher代码段

提示:后面的东西需要一些耐心。

了解一些 Spring Boot 的抽象概念

了解一下 Spring Boot Loader 所抽象出来的一些概念,对走读 Spring Boot loader 源码有些帮助

Launcher:各种 Launcher 的基础抽象类,用于启动应用程序,跟Archive配合使用。

目前有3种实现,分别是

  1. JarLauncher
  2. WarLauncher
  3. PropertiesLauncher

继承关系如下

Launcher继承关系

Archive:归档文件的基础抽象类。

  1. JarFileArchive 就是 jar 包文件的抽象。

    它提供了一些方法比如 getUrl 会返回这个 Archive 对应的 URL。getManifest 方法会获得 Manifest 数据等。

  2. ExplodedArchive 是文件目录的抽象。

JarFile:对 jar 包的封装,每个 JarFileArchive 都会对应一个 JarFile。JarFile 被构造的时候会解析内部结构,去获取 jar 包里的各个文件或文件夹,这些文件或文件夹会被封装到 Entry 中,也存储在 JarFileArchive 中。如果 Entry 是个jar,会解析成 JarFileArchive。

JarFile是Springboot-loader 继承 JDK JarFile提供的类。

比如一个 JarFileArchive 对应的URL为:

jar:file:C:\Users\Administrator\Desktop\demo\demo\target\jarlauncher-0.0.1-SNAPSHOT.jar!/

它对应的 JarFile 为:

C:\Users\Administrator\Desktop\demo\demo\target\jarlauncher-0.0.1-SNAPSHOT.jar

这个 JarFile 有很多Entry,比如:

META-INF/
META-INF/MANIFEST.MF
......
BOOT-INF/lib/spring-boot-starter-1.5.10.RELEASE.jar
BOOT-INF/lib/spring-boot-1.5.10.RELEASE.jar
...

JarFileArchive 内部的一些依赖 jar 对应的 URL

(SpringBoot 使用 org.springframework.boot.loader.jar.Handler 处理器来处理这些URL)

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我们看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包里面的 class 文件,那么会使用 !/ 分隔开,这种方式只有 org.springframework.boot.loader.jar.Handler 能处理,它是 SpringBoot 内部扩展出来一种URL协议其实这个非常重要,对于后面说的自定义加载器,拓展URL协议是基石)。

可执行 jar 目录结构

注意:咱们以 Spring Boot 1.5.10 版本来分析

本来想直接用 Spring Boot 2.3.x 作为 debug 环境的,也看了一圈网文,发现比 2.3.x 比 1.x 版本多了一些概念,比如分层的JarModel,自己又不会,弄过来直接搪塞过去也不太好,就先放弃了,最终使用不算太老的 1.5.10 版本。

SpringBoot 提供了一个插件 spring-boot-maven-plugin 用于把程序打包成一个可执行的jar包

在 pom 文件里加入这个插件即可:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

然后我们在 Terminal 执行 maven package 打包完生成的 jarlauncher-0.0.1-SNAPSHOT.jar (我们称之为 Fat jar)内部的结构如下:

├─BOOT-INF
│  ├─classes
│  │  └─application.properties
│  │  └─com
│  │      └─example
│  │          └─jarlauncher
│  │              └─JarlauncherApplication.class
│  └─lib
│      ├─spring-boot-1.5.10.RELEASE.jar
│      ├─spring-boot-loader-1.5.10.RELEASE.jar
│      ├─.......
├─META-INF
│  └─MANIFEST.MF
│  └─maven
│      └─com.example
│          └─demo
│              ├─pom.properties
│              ├─pom.xml
└─org
    └─springframework
        └─boot
            └─loader
                ├─ExecutableArchiveLauncher.class
                ├─JarLauncher.class
                ├─LaunchedURLClassLoader.class
                ├─Launcher.class
                ├─MainMethodRunner.class
            └─......

打包出来 fat jar 内部有三个文件夹:

  1. META-INF 文件夹:程序入口,其中 MANIFEST.MF(资源清单) 用于描述 jar 包的信息
  2. BOOT-INF 目录:放置我们的程序代码和第三方依赖的jar包
  3. org目录:Spring Boot loader 相关的源代码,我们程序启动就靠他了

MANIFEST.MF文件的内容:

Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: Administrator
Implementation-Vendor-Id: com.example
Spring-Boot-Version: 1.5.10.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.jarlauncher.JarlauncherApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_162
Implementation-URL: http://projects.spring.io/spring-boot/demo/

我们看到,它的 Main-Class 是 org.springframework.boot.loader.JarLauncher,当我们使用 java -jar 执行jar包的时候会调用 JarLauncher 的main方法,而不是调用我们编写的 com.example.jarlauncher.JarlauncherApplication

接下来咱们走读一下代码,看看实际怎么运行的吧!

JarLauncher的执行过程

提示:走读的时候时不时结合概览中的时序图,可能好些。

JarLauncher 的 main 方法:

public static void main(String[] args) {
    // 构造JarLauncher,然后调用它的launch方法
    new JarLauncher().launch(args);
}

JarLauncher 被构造的时候会调用父类 ExecutableArchiveLauncher 的构造方法。

ExecutableArchiveLauncher 的构造方法内部会去构造 Archive,这里构造了 JarFileArchive。构造JarFileArchive的过程中还会构造很多东西,比如JarFile,Entry …

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    // 构造器会初始化代表 fat jar 的 Archive
    public ExecutableArchiveLauncher() {
        this.archive = createArchive();
    }
    // 由父类 Launcher 实现
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        // 最终会 new 一个 Arichive,内部生产的 JarFile-->这个逼对FatJar资源加载非常重要
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
    
    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> archives = new ArrayList<Archive>(
            // 获取内部所有有的 Arichive
            this.archive.getNestedArchives(new EntryFilter() {
                @Override
                public boolean matches(Entry entry) {
                    return isNestedArchive(entry);
                }
            }));
        // 空实现,没用
        postProcessClassPathArchives(archives);
        return archives;
    }

}

JarLauncher 的 launch方法:

protected void launch(String[] args) {
  try {
// 在系统属性中设置注册了自定义的URL协议处理器:org.springframework.boot.loader.jar.Handler。
// 初始化URL的时候,如果URL中没有指定处理器,会去系统属性中查询
    JarFile.registerUrlProtocolHandler();
// getClassPathArchives方法会去找lib目录下对应的第三方依赖JarFileArchive,同时也会找项目自身的JarFileArchive
// 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath
// 多说两句句,
// 1.URLClassPath这个属性很重要,自定义ClassLoader,findClass就靠它了!
// 2.可以关注一下构造LaunchedURLClassLoader时,archive.getUrl方法,这里就涉及到自定义URL协议处理器了,JarFile等。毕竟实现jar in jar功能靠他们这些小罗罗。
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
// getMainClass方法会去项目自身的Archive中的Manifest中找出key为Start-Class的类
// 调用重载方法launch
    launch(args, getMainClass(), classLoader);
  }
  catch (Exception ex) {
    ex.printStackTrace();
    System.exit(1);
  }
}

// Archive的getMainClass方法,不过由ExecutableArchiveLauncher实现
// 这里会找出Start-Class标识的com.example.jarlauncher.JarlauncherApplication这个类
public String getMainClass() throws Exception {
    Manifest manifest = getManifest();
    String mainClass = null;
    if (manifest != null) {
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
        throw new IllegalStateException(
                "No 'Start-Class' manifest entry specified in " + this);
    }
    return mainClass;
}

// launch重载方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
    throws Exception {
    // 设置 LaunchedURLClassLoader 为线程上下文加载器
    Thread.currentThread().setContextClassLoader(classLoader);
    // 创建一个MainMethodRunner 并运行
    createMainMethodRunner(mainClass, args, classLoader).run();
}

MainMethodRunner 的 run 方法:

public void run() throws Exception {
    // 使用线程上下文类加载器加载主类
    Class<?> mainClass = Thread.currentThread().getContextClassLoader()
    .loadClass(this.mainClassName);
    // 反射执行,至此咱们的应用程序就启动起来啦,good,启动流程走读结束,开心!可以跟面试官扯些了
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    mainMethod.invoke(null, new Object[] { this.args });
}

Start-Class的main方法调用之后,内部会构造Spring容器,启动内置Servlet容器等过程(后面的就不说了,不是本文关注的点,况且也没细研究呢😂)

好了,到这里咱们已经把 java -jar 的启动过程整体了解了一遍,开心吧!

关于自定义的类加载器

看看传说中的 LaunchedURLClassLoader有什么神奇的

LaunchedURLClassLoader 重写了 loadClass 方法,走读一下

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    Handler.setUseFastConnectionExceptions(true);
    try {
        try {
            // 在调用 findClass 之前定义 package,确保嵌套JAR清单与包相关联
            definePackageIfNecessary(name);
        }
        catch (IllegalArgumentException ex) {
            if (getPackage(name) == null) {
                throw new AssertionError("Package " + name + " has already been "
                        + "defined but it could not be found");
            }
        }
        // 调用 父类 loadClass 走正常的加载委派流程
        return super.loadClass(name, resolve);
    }
    finally {
        Handler.setUseFastConnectionExceptions(false);
    }
}
其实只看上面 1.5.10 版本的 loadClass 实现,毫无亮点,基本就是普通的双亲委派过程。

而且 LaunchedURLClassLoader 使用的 findClass 是从父类 URLClassLoader 继承的。

最终 loadClass 会走到 LaunchedURLClassLoader 的父类 URLClassLoader#findClass

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
// 把类名解析成路径并加上.class后缀                         
                        String path = name.replace('.', '/').concat(".class");
// 基于之前得到的第三方jar包依赖以及自己的jar包得到URL数组,进行遍历找出对应类名的资源
// 比如path是org/springframework/boot/loader/JarLauncher.class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/中被找出
// 那么找出的资源对应的URL为jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class                        
                    // 加载fatjar class的关键部分!!!
                    Resource res = ucp.getResource(path, false);
                    if (res != null) { // 找到了资源
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

上面的findClass 的过程,都是在关键代码 Resource res = ucp.getResource(path, false); 这里完成的。

ucp 也即 JDK 提供的 sun.misc.URLClassPath

又画了个图,可以看到URLClassPath#getResource涉及哪些基础组件支持。

会用到 URL,URLStreamHandler,org.springframework.boot.loader.jar.Handler,最终获取到 Resource,完成 class load。

findClass

所以,

个人结论:LaunchedURLClassLoader 是借助他山之力,关键还在于 Spring Boot 对 URL jar 协议的拓展,Archeive,JarFile 的抽象

LaunchedURLClassLoader 加载测试

咱们手动模拟一下 JarLauncher 的加载过程,创建 LaunchedURLClassLoader,然后加载个类试试好不好使?

public class LaunchedURLClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 注册org.springframework.boot.loader.jar.Handler URL协议处理器
        JarFile.registerUrlProtocolHandler();
        // 构造LaunchedURLClassLoader类加载器,这里使用了1个URL,对应jar包中依赖包spring-boot-loader       
        // 会使用 org.springframework.boot.loader.jar.Handler 处理器处理
        LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
                new URL[]{
                        new URL("jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.5.10.RELEASE.jar!/")
                },
                DemoApplication.class.getClassLoader());
        // 加载类
        classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
    }
}
把这个 case 跑通之后,JarLauncher 的启动流程就没啥问题了吧?

赠送一个 IDEA Debug Fat Jar 启动的环境

说了这么多启动流程,如何才能直观的 debug 到 Spring Boot Loader 的执行过程呢?

下面咱们就来做这事,很简单,几分钟搞定。

代码准备

直接在 start.spring.io 初始化一个的 SpringBoot 应用就行,版本改成 1.5.10。

我这给个 Git 代码模板吧,点击去克隆

注意一点,maven 要添加 spring-boot-loader 的依赖,一起打到 jar 里去。

<!-- Spring Boot loader        -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
</dependency>

然后 mvn package,把应用打包成可执行 jar。

IDEA 配置

1、配置以 Jar 应用的方式启动

image-20200613172706071

2、配置 Jar 路径,然后 Apply

image-20200613172529276

3、找到启动类 JarLauncher,打上断点,debug 方式启动

image-20200613192631035

References

springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载--推荐阅读研究

SpringBoot可执行jar包启动原理

完结,撒花。

你学会了吗?

查看原文

赞 0 收藏 0 评论 0

猴哥一一 发布了文章 · 6月7日

[安利] WSL Linux 子系统,真香!附完整实操

WSL 初体验

WSL Linux 子系统体验原生 Docker,真香!

Windows 的 linux 子系统出来挺长时间了,你体验过了吗?

今天就带你折腾一下吧,毕竟想甩掉超占用硬件资源的大块头VM,比如VMware

本文献给爱折腾的你,折腾吧,后浪!

简单说下这篇文章的重点:

  • 安装 WSL 的全过程
  • 选择性升级到 WSL 2 的全过程
  • WSL 2 中体验原生 Docker

啥是 WSL ?

WSL 是 Windows Subsystem for Linux 的缩写,意思是 linux 版的 window 子系统。

引用自:微软官网 https://docs.microsoft.com/zh...

The Windows Subsystem for Linux lets developers run a GNU/Linux environment -- including most command-line tools, utilities, and applications -- directly on Windows, unmodified, without the overhead of a virtual machine.

You can:

  • Choose your favorite GNU/Linux distributions from the Microsoft Store.
  • Run common command-line tools such as grep, sed, awk, or other ELF-64 binaries.
  • Run Bash shell scripts and GNU/Linux command-line applications including:

    • Tools: vim, emacs, tmux Languages: NodeJS, Javascript, Python, Ruby, C/C++, C# & F#, Rust, Go, etc. Services: SSHD, MySQL, Apache, lighttpd, MongoDB, PostgreSQL.
  • Install additional software using own GNU/Linux distribution package manager.
  • Invoke Windows applications using a Unix-like command-line shell.
  • Invoke GNU/Linux applications on Windows

简单的说就是,Linux 的 Windows 子系统让开发人员无需虚拟机就可以直接在 Windows 上运行 Linux 环境,包括大多数命令行工具、程序和应用。

使用 WSL 的好处是:

  1. 与在虚拟机下使用 Linux 相比,WSL 占用资源更少,更加流畅;
  2. WSL 可以对 Windows 文件系统下的文件直接进行读写,文件传输更方便;
  3. 剪贴板互通,可以直接在 Windows 下其它地方复制文本内容,粘贴到 WSL;

备注:其实我挺喜欢虚拟机的,毕竟是模拟了硬件,比较成熟,稳定。

开启WSL支持

使用管理员权限的 Shell 才能安装 WSL。

按 Win+X, 找到 Windows PowerShell (管理员),并复制执行命令。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

以上命令会激活 WSL 服务,然后需要重启系统

重启之后,Win + R,输入 appwiz.cpl,左上角找到“启动或关闭 Windows 功能”,会看到这个选项处于选中状态。

其实吧,上面的命令就相当于手动去勾选这个功能。

直接命令执行效率可能更高些。

image-20200606122027686

安装 WSL 发行版

在 Windows 应用商店搜索 ubuntu ,选择自己喜欢的版本,安装即可。

这里我选择的是 Ubuntu 20.04 LTS,之后的所有内容也是基于 WSL Ubuntu 编写。

image-20200606122740396

下载,安装之后,第一次打开会初始化一会

然后设置个用户名,密码

image-20200606125309524

到这里,其实咱们的 WSL 就安装好了。

接下来我们让它更好用吧!

apt 换源

Debian / Ubuntu 的官方源在国内访问很慢,咱们更换为清华大学 TUNA 的软件源镜像

PS:也可以用阿里云的镜像,我体验了没那么快(可能我姿势不对),就不推荐了。

  • 执行下面命令,备份 apt 安装源:
$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
  • vim 编辑 sources.list :
$ sudo vim /etc/apt/sources.list 
  • 将 sources.list 中的内容替换如下

注意:这里是Ubuntu 20.04 LTS的,

如果是其他版本的ubuntu,自行访问[ 清华大学开源软件镜像站]去查找对应版本的镜像配置

# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse

# 预发布软件源,不建议启用
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse

更新软件

  • 更新软件源中的所有软件列表,可以看到飞快的在刷屏,哗哗哗的
$ sudo apt-get update
  • 更新软件
$ sudo apt-get upgrade
# 更新内容稍多,差不多几分钟,玩会手机吧。。。

配置SSH服务器

咱们用惯了 类似 xshell ,SecureCRT 这种 SSH 工具连接 Linux,所以顺便把 SSH 功能打通吧,用的顺手,毕竟 PowerShell 用的不是太方便!

WSL 上的 SSH 服务器没有自动配置,需要手动重新安装,首先可以运行以下命令来检查

$ sudo service ssh stop
$ sudo /usr/sbin/sshd -d

image-20200607000536951

如果输出信息包括以上信息,即找不到 key,重新安装 openssh-server 就可以解决问题

$ sudo apt purge openssh-server
$ sudo apt install openssh-server

然后需要配置 /etc/ssh/sshd_config,用 sudo 权限运行 vim 修改如下三个关键字

记得删除 #

Port 22
# 这两行允许了 root 账户和密码登录
PermitRootLogin yes
PasswordAuthentication yes

image-20200607001214265

然后记得重启 ssh 服务

$ sudo service ssh restart
$ sudo service ssh status

如果需要用密码登录 root 账户,还需要设置密码

$ sudo passwd root

然后就可以使用 SSH 工具进行连接啦,本地直接 localhost 即可

image-20200607001637550


OK,到此,咱们的 WSL 已经配置完毕。

尽情体验,安装 Nginx,Redis,MySQL .... 折腾吧后浪!


这就完了???

对,差不多就完了。

以上内容就是 WSL 安装的全部内容。

客官要体验下 WSL2 吗?

去年Build大会,WSL2 正式推出。

WSL2 附带了一个真实的 Linux 4.19 内核,能够带来完整的系统调用兼容性,并且能够直接借助自动更新进行升级维护,无需更新整个Windows Linux的子系统。

同时,WSL2将比第一代的WSL1版本运行速度更快,提升了文件系统的I/O性能和与Linux的兼容性,且可本机直接运行 Docker 容器等这点我喜欢,嘿嘿)。

那么咱们来查看一下我们上面安装的 WSL 版本,

执行命令 wsl -l -v,如果是这个结果,那么恭喜你呀,WSL 1!

image-20200607102652775

什么?我的结果不是这样???怎么我执行命令控制台提示什么无效的命令选项????
类似下面这样的结果,没事,我知道你会这样,毕竟我是一步步爬着过来的,跟我一起继续往下看。

image-20200607111719671

那么,咱们接下来说,WSL2 使用是有门槛的

你的系统,需要是满足版本的内部版本,强调一下内部版本,其实就是预览(不稳定)版本。

image-20200607112203509

所以,下面要不要升级到 WSL2,有两方面考虑,

  • 一个是你的 windows 版本能不能跟的上
  • 另一个你能不能承担升级预览版本带来的风险?

如果满足不了,就折腾 WSL 1 也挺好,最起码体验一把 windows 的进步。

先升级到内部预览版本

步骤就不写了
百度找一篇给你【Windows】WIN10如何获取内部预览版本

注意:不是你填个信息申请一下立马就可以用到内部预览版了,要等 windows 推送给你,我大概等了三四天,发现有更新了,然后更新后再执行 wsl -l -v,就正常了。

image-20200607114436053

更新后,主界面,右下角,会有一些内部预览版的标识。。。

image-20200607214630606

准备好了吗?下面咱们开始更新到 WSL2 吧,跟我一起不停的重启电脑。。。

更新到 WSL 2

启用“虚拟机平台”可选组件

安装 WSL 2 之前,必须启用“虚拟机平台”可选功能。

以管理员身份打开 PowerShell 并运行:

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

image-20200607112931349

重新启动计算机,以完成 WSL 安装并更新到 WSL 2。

将 WSL 2 设置为默认版本

在 Powershell 中运行以下命令,将 WSL 2 设置为默认版本:

# 1.WSL 提供了版本转换工具,将 wsl1 转为 wsl2,
# 注意转换时间可能较长,耐心等待,如果时间太久没反应,时不时按下 Enter 看看有没有更新
wsl --set-version Ubuntu-20.04 2
# 2.将 WSL2 设置为默认版本
wsl --set-default-version 2

image-20200607134904093

现在我们运行wsl -l -v,看到 VERSION 已经是 2 了!

再次恭喜,我们已经成功将 WSL 1 升级为 WSL 2 了!!!

一个真正的 Linux 内核的系统已经在你的 Windows 里了!

在WSL2子系统Ubuntu中安装Docker-CE

其实我升级WSL 2 的目的,是想体验原版的 Docker 的,哈哈,来吧,既然都跟到这里了,就开始吧!
安装 Docker-CE
$ curl -skSL https://mirror.azure.cn/repo/install-docker-ce.sh | sh -s -- --mirror AzureChinaCloud
http://mirror.azure.cn/help/d...

image-20200607154054964

启动Docker,查看 Docker 版本
$ sudo service docker start
$ sudo docker version
跑个应用?

你说你装好了,倒是跑个应用看看呀?

好吧,

像 Docker 官网的安装步骤一样,也会有这么一步,通过运行 hello-world 映像来验证 Docker Engine 是否已正确安装。

$ sudo docker run hello-world

image-20200607155649450

我成功了,你呢?

感兴趣可以跟下来实操哦,毕竟我是一点点坑爬上来的才有这篇文章,为了复原真实步骤,装了两次 WSL。

只点赞或收藏等于学会?不存在的,实操一下吧!

查看原文

赞 2 收藏 2 评论 0

猴哥一一 收藏了文章 · 6月2日

springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载

在上篇文章《springboot应用启动原理(一) 将启动脚本嵌入jar》中介绍了springboot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得生成的jar/war文件可以直接启动
本篇将介绍springboot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动我们的应用。

本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 环境

首先,从一个简单的示例开始

build.gradle

group 'com.manerfan.spring'
version '1.0.0'

apply plugin: 'java'
apply plugin: 'java-library'

sourceCompatibility = 1.8

buildscript {
    ext {
        springBootVersion = '2.0.0.RELEASE'
    }

    repositories {
        mavenLocal()
        maven {
            name 'aliyun maven central'
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    launchScript()
}

repositories {
    mavenLocal()
    maven {
        name 'aliyun maven central'
        url 'http://maven.aliyun.com/nexus/content/groups/public'
    }
}

dependencies {
    api 'org.springframework.boot:spring-boot-starter-web'
}

WebApp.java

@SpringBootApplication
@RestController
public class WebApp {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

执行gradle build构建jar包,里面包含应用程序第三方依赖以及springboot启动程序,其目录结构如下

spring-boot-theory-1.0.0.jar
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

查看MANIFEST.MF的内容(MANIFEST.MF文件的作用请自行GOOGLE)

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到,jar的启动类为org.springframework.boot.loader.JarLauncher,而并不是我们的com.manerfan.springboot.theory.WebApp,应用程序入口类被标记为了Start-Class

jar启动并不是通过应用程序入口类,而是通过JarLauncher代理启动。其实SpringBoot拥有3中不同的Launcher:JarLauncherWarLauncherPropertiesLauncher

launcher

springboot使用Launcher代理启动,其最重要的一点便是可以自定义ClassLoader,以实现对jar文件内(jar in jar)或其他路径下jar、class或资源文件的加载
关于ClassLoader的更多介绍可参考《深入理解JVM之ClassLoader》

Archive

  • 归档文件
  • 通常为tar/zip等格式压缩包
  • jar为zip格式归档文件

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。

上例中,spring-boot-theory-1.0.0.jar既为一个JarFileArchive,spring-boot-theory-1.0.0.jar!/BOOT-INF/lib下的每一个jar包也是一个JarFileArchive
将spring-boot-theory-1.0.0.jar解压到目录spring-boot-theory-1.0.0,则目录spring-boot-theory-1.0.0为一个ExplodedArchive

public interface Archive extends Iterable<Archive.Entry> {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

JarLancher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class

其实JarLauncher实现很简单

public class JarLauncher extends ExecutableArchiveLauncher {
    public JarLauncher() {}
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序
再创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // 找到自己所在的jar,并创建Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用

public abstract class Launcher {
    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        // 生成自定义ClassLoader
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
        // 启动应用
        launch(args, getMainClass(), classLoader);
    }

    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
            throws Exception {
        // 将自定义ClassLoader设置为当前线程上下文类加载器
        Thread.currentThread().setContextClassLoader(classLoader);
        // 启动应用
        createMainMethodRunner(mainClass, args, classLoader).run();
    }
}

public abstract class ExecutableArchiveLauncher extends Launcher {
    protected List<Archive> getClassPathArchives() throws Exception {
        // 获取/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录对应的archive
        List<Archive> archives = new ArrayList<>(
                this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }
}

public class MainMethodRunner {
    // Start-Class in MANIFEST.MF
    private final String mainClassName;

    private final String[] args;

    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args == null ? null : args.clone());
    }

    public void run() throws Exception {
        // 加载应用程序主入口类
        Class<?> mainClass = Thread.currentThread().getContextClassLoader()
                .loadClass(this.mainClassName);
        // 找到main方法
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        // 调用main方法,并启动
        mainMethod.invoke(null, new Object[] { this.args });
    }
}

至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载

LaunchedURLClassLoader

在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler

URLStreamHandler

java中定义了URL的概念,并实现多种URL协议(见URLhttpfileftpjar 等,结合对应的URLConnection可以灵活地获取各种协议下的资源

public URL(String protocol,
           String host,
           int port,
           String file,
           URLStreamHandler handler)
    throws MalformedURLException

对于jar,每个jar都会对应一个url,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的资源,也会对应一个url,并以'!/'分割,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如
jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

自定义URL的类格式为[pkgs].[protocol].Handler,在运行Launcher的launch方法时调用了JarFile.registerUrlProtocolHandler()以注册自定义的 Handler

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
public static void registerUrlProtocolHandler() {
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
            : handlers + "|" + HANDLERS_PACKAGE));
    resetCachedUrlHandlers();
}

在处理如下URL时,会循环处理'!/'分隔符,从最上层出发,先构造spring-boot-theory.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的
JarURLConnection ,通过JarURLConnection的getInputStream方法获取SpringProxy.class内容

jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

从一个URL,到读取其中的内容,整个过程为

  • 注册一个Handler处理‘jar:’这种协议
  • 扩展JarFile、JarURLConnection,处理jar in jar的情况
  • 循环处理,找到内层资源
  • 通过getInputStream获取资源内容

URLClassLoader可以通过原始的jar协议,加载jar中从class文件
LaunchedURLClassLoader 通过扩展的jar协议,以实现jar in jar这种情况下的class文件加载

WarLauncher

构建war包很简单

  1. build.gradle中引入插件 apply plugin: 'war'
  2. build.gradle中将内嵌容器相关依赖设为providedprovidedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
  3. 修改WebApp内容,重写SpringBootServletInitializer的configure方法
@SpringBootApplication
@RestController
public class WebApp extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WebApp.class);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

构建出的war包,其目录机构为

spring-boot-theory-1.0.0.war
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   │   └── 应用程序
│   └── lib
│       └── 第三方依赖jar
│   └── lib-provided
│       └── 与内嵌容器相关的第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

MANIFEST.MF内容为

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.WarLauncher

此时,启动类变为了org.springframework.boot.loader.WarLauncher,查看WarLauncher实现,其实与JarLauncher并无太大差别

public class WarLauncher extends ExecutableArchiveLauncher {
    private static final String WEB_INF = "WEB-INF/";
    private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
    private static final String WEB_INF_LIB = WEB_INF + "lib/";
    private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";

    public WarLauncher() {
    }

    @Override
    public boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(WEB_INF_CLASSES);
        }
        else {
            return entry.getName().startsWith(WEB_INF_LIB)
                    || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
        }
    }

    public static void main(String[] args) throws Exception {
        new WarLauncher().launch(args);
    }
}

差别仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar

如此依赖,构建出的war便支持两种启动方式

  • 直接运行./spring-boot-theory-1.0.0.war start
  • 部署到Tomcat容器下

PropertiesLauncher

PropretiesLauncher 的实现与 JarLauncher WarLauncher 的实现极为相似,通过PropretiesLauncher可以实现更为轻量的thin jar,其实现方式可自行查阅源码

总结

  • SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载
  • SpringBoot通过扩展URLClassLoader--LauncherURLClassLoader,实现了jar in jar中class文件的加载
  • JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动
  • WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动

订阅号

查看原文

猴哥一一 赞了文章 · 6月2日

springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载

在上篇文章《springboot应用启动原理(一) 将启动脚本嵌入jar》中介绍了springboot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得生成的jar/war文件可以直接启动
本篇将介绍springboot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动我们的应用。

本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 环境

首先,从一个简单的示例开始

build.gradle

group 'com.manerfan.spring'
version '1.0.0'

apply plugin: 'java'
apply plugin: 'java-library'

sourceCompatibility = 1.8

buildscript {
    ext {
        springBootVersion = '2.0.0.RELEASE'
    }

    repositories {
        mavenLocal()
        maven {
            name 'aliyun maven central'
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    launchScript()
}

repositories {
    mavenLocal()
    maven {
        name 'aliyun maven central'
        url 'http://maven.aliyun.com/nexus/content/groups/public'
    }
}

dependencies {
    api 'org.springframework.boot:spring-boot-starter-web'
}

WebApp.java

@SpringBootApplication
@RestController
public class WebApp {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

执行gradle build构建jar包,里面包含应用程序第三方依赖以及springboot启动程序,其目录结构如下

spring-boot-theory-1.0.0.jar
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

查看MANIFEST.MF的内容(MANIFEST.MF文件的作用请自行GOOGLE)

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到,jar的启动类为org.springframework.boot.loader.JarLauncher,而并不是我们的com.manerfan.springboot.theory.WebApp,应用程序入口类被标记为了Start-Class

jar启动并不是通过应用程序入口类,而是通过JarLauncher代理启动。其实SpringBoot拥有3中不同的Launcher:JarLauncherWarLauncherPropertiesLauncher

launcher

springboot使用Launcher代理启动,其最重要的一点便是可以自定义ClassLoader,以实现对jar文件内(jar in jar)或其他路径下jar、class或资源文件的加载
关于ClassLoader的更多介绍可参考《深入理解JVM之ClassLoader》

Archive

  • 归档文件
  • 通常为tar/zip等格式压缩包
  • jar为zip格式归档文件

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。

上例中,spring-boot-theory-1.0.0.jar既为一个JarFileArchive,spring-boot-theory-1.0.0.jar!/BOOT-INF/lib下的每一个jar包也是一个JarFileArchive
将spring-boot-theory-1.0.0.jar解压到目录spring-boot-theory-1.0.0,则目录spring-boot-theory-1.0.0为一个ExplodedArchive

public interface Archive extends Iterable<Archive.Entry> {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

JarLancher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class

其实JarLauncher实现很简单

public class JarLauncher extends ExecutableArchiveLauncher {
    public JarLauncher() {}
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序
再创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // 找到自己所在的jar,并创建Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用

public abstract class Launcher {
    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        // 生成自定义ClassLoader
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
        // 启动应用
        launch(args, getMainClass(), classLoader);
    }

    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
            throws Exception {
        // 将自定义ClassLoader设置为当前线程上下文类加载器
        Thread.currentThread().setContextClassLoader(classLoader);
        // 启动应用
        createMainMethodRunner(mainClass, args, classLoader).run();
    }
}

public abstract class ExecutableArchiveLauncher extends Launcher {
    protected List<Archive> getClassPathArchives() throws Exception {
        // 获取/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录对应的archive
        List<Archive> archives = new ArrayList<>(
                this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }
}

public class MainMethodRunner {
    // Start-Class in MANIFEST.MF
    private final String mainClassName;

    private final String[] args;

    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args == null ? null : args.clone());
    }

    public void run() throws Exception {
        // 加载应用程序主入口类
        Class<?> mainClass = Thread.currentThread().getContextClassLoader()
                .loadClass(this.mainClassName);
        // 找到main方法
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        // 调用main方法,并启动
        mainMethod.invoke(null, new Object[] { this.args });
    }
}

至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载

LaunchedURLClassLoader

在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler

URLStreamHandler

java中定义了URL的概念,并实现多种URL协议(见URLhttpfileftpjar 等,结合对应的URLConnection可以灵活地获取各种协议下的资源

public URL(String protocol,
           String host,
           int port,
           String file,
           URLStreamHandler handler)
    throws MalformedURLException

对于jar,每个jar都会对应一个url,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的资源,也会对应一个url,并以'!/'分割,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如
jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

自定义URL的类格式为[pkgs].[protocol].Handler,在运行Launcher的launch方法时调用了JarFile.registerUrlProtocolHandler()以注册自定义的 Handler

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
public static void registerUrlProtocolHandler() {
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
            : handlers + "|" + HANDLERS_PACKAGE));
    resetCachedUrlHandlers();
}

在处理如下URL时,会循环处理'!/'分隔符,从最上层出发,先构造spring-boot-theory.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的
JarURLConnection ,通过JarURLConnection的getInputStream方法获取SpringProxy.class内容

jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

从一个URL,到读取其中的内容,整个过程为

  • 注册一个Handler处理‘jar:’这种协议
  • 扩展JarFile、JarURLConnection,处理jar in jar的情况
  • 循环处理,找到内层资源
  • 通过getInputStream获取资源内容

URLClassLoader可以通过原始的jar协议,加载jar中从class文件
LaunchedURLClassLoader 通过扩展的jar协议,以实现jar in jar这种情况下的class文件加载

WarLauncher

构建war包很简单

  1. build.gradle中引入插件 apply plugin: 'war'
  2. build.gradle中将内嵌容器相关依赖设为providedprovidedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
  3. 修改WebApp内容,重写SpringBootServletInitializer的configure方法
@SpringBootApplication
@RestController
public class WebApp extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WebApp.class);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

构建出的war包,其目录机构为

spring-boot-theory-1.0.0.war
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   │   └── 应用程序
│   └── lib
│       └── 第三方依赖jar
│   └── lib-provided
│       └── 与内嵌容器相关的第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

MANIFEST.MF内容为

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.WarLauncher

此时,启动类变为了org.springframework.boot.loader.WarLauncher,查看WarLauncher实现,其实与JarLauncher并无太大差别

public class WarLauncher extends ExecutableArchiveLauncher {
    private static final String WEB_INF = "WEB-INF/";
    private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
    private static final String WEB_INF_LIB = WEB_INF + "lib/";
    private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";

    public WarLauncher() {
    }

    @Override
    public boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(WEB_INF_CLASSES);
        }
        else {
            return entry.getName().startsWith(WEB_INF_LIB)
                    || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
        }
    }

    public static void main(String[] args) throws Exception {
        new WarLauncher().launch(args);
    }
}

差别仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar

如此依赖,构建出的war便支持两种启动方式

  • 直接运行./spring-boot-theory-1.0.0.war start
  • 部署到Tomcat容器下

PropertiesLauncher

PropretiesLauncher 的实现与 JarLauncher WarLauncher 的实现极为相似,通过PropretiesLauncher可以实现更为轻量的thin jar,其实现方式可自行查阅源码

总结

  • SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载
  • SpringBoot通过扩展URLClassLoader--LauncherURLClassLoader,实现了jar in jar中class文件的加载
  • JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动
  • WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动

订阅号

查看原文

赞 14 收藏 11 评论 5

猴哥一一 发布了文章 · 6月2日

[Redis] 你了解 Redis 的三种集群模式吗?

最近在面试过程中被面试官问到 Redis 集群数据是如何复制的,由于之前没有准备直接懵了。

事后查了查这个问题其实也挺简单,如果你之前也不知道,没问题,赶紧浅尝辄止,速度3遍即可入门。

阅读本文,你可能会有哪些收获呢?

  • 首先,你会知道有三种集群模式
  • 然后对每种集群模式的原理有个大概了解
  • 当然还能看到集群演变的影子
  • 最后还会有手把手的实操

Redis 支持三种集群方案

  • 主从复制模式
  • Sentinel(哨兵)模式
  • Cluster 模式

Redis 集群的三种模式

主从复制模式

img

主从复制的作用

通过持久化功能,Redis保证了即使在服务器重启的情况下也不会丢失(或少量丢失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。 但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。

为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。

为此, Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上

在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

总结:引入主从复制机制的目的有两个

  • 一个是读写分离,分担 "master" 的读写压力
  • 一个是方便做容灾恢复

主从复制原理

  • 从数据库启动成功后,连接主数据库,发送 SYNC 命令;
  • 主数据库接收到 SYNC 命令后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;
  • 主数据库 BGSAVE 执行完后,向所有从数据库发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从数据库收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令;
  • 从数据库完成对快照的载入,开始接收命令请求,并执行来自主数据库缓冲区的写命令;(从数据库初始化完成
  • 主数据库每执行一个写命令就会向从数据库发送相同的写命令,从数据库接收并执行收到的写命令(从数据库初始化完成后的操作
  • 出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库,增量复制。
  • 主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主从复制优缺点

主从复制优点

  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
  • 为了分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成;
  • Slave 同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力;
  • Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在 Master-Slave 同步期间,客户端仍然可以提交查询或修改请求;
  • Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据;

主从复制缺点

  • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复(也就是要人工介入);
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
  • 如果多个 Slave 断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要 Slave 启动,就会发送sync 请求和主机全量同步,当多个 Slave 重启的时候,可能会导致 Master IO 剧增从而宕机。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;

Sentinel(哨兵)模式

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个 Redis 实例

单哨兵

哨兵模式的作用

  • 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;
  • 当哨兵监测到 master 宕机,会自动将 slave 切换成 master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;

然而一个哨兵进程对Redis服务器进行监控,也可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

多哨兵

故障切换的过程

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

哨兵模式的工作方式:

  • 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
  • 如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态
  • 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)
  • 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、Slave 从服务器发送 INFO 命令。
  • 当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  • 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

哨兵模式的优缺点

优点:

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高(可以看作自动版的主从复制)。

缺点:

  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Cluster 集群模式(Redis官方)

Redis Cluster是一种服务器 Sharding 技术,3.0版本开始正式提供。

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容

image-20200531184321294

在这个图中,每一个蓝色的圈都代表着一个 redis 的服务器节点。它们任何两个节点之间都是相互连通的。客户端可以与任何一个节点相连接,然后就可以访问集群中的任何一个节点。对其进行存取和其他操作。

集群的数据分片

Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念。

Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5460 号哈希槽
  • 节点 B 包含 5461 到 10922 号哈希槽
  • 节点 C 包含 10923 到 16383 号哈希槽

这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

Redis 集群的主从复制模型

为了保证高可用,redis-cluster集群引入了主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了,那么该集群就无法再提供服务了。

集群的特点

  • 所有的 redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  • 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。
  • 客户端与 Redis 节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

理论课结束了,不如实操下感受一下?

手把手体验集群配置

前提条件

  • 安装 redis, 我从Redis 官网下载的最新版 redis-5.0.5
  • linux 环境,我用的 centos 7.7, VM 环境
# redis 准备
$ cd /opt
$ wget http://download.redis.io/releases/redis-5.0.5.tar.gz
$ tar xzf redis-5.0.5.tar.gz
$ cd redis-5.0.5
$ make
$ make install

生产环境做集群一般会采用多个独立主机,这里做演示在一台虚拟机上同时运行多个节点的,这点注意一下。

主从复制

主要有两步

  • 准备 master/slave 配置文件
  • 先启动 master 再启动 slave,进行验证
集群规划
节点配置文件端口
masterredis6379.conf6379
slave1redis6380.conf6380
slave1redis6381.conf6380
配置文件

内容如下

# redis6379.conf    master
# 包含命令,有点复用的意思
include /opt/redis-5.0.5/redis.conf
pidfile /var/run/redis_6379.pid
port    6379
dbfilename dump6379.rdb
logfile "my-redis-6379.log"

# redis6380.conf    slave1
include /opt/redis-5.0.5/redis.conf
pidfile /var/run/redis_6380.pid
port    6380
dbfilename dump6380.rdb
logfile "my-redis-6380.log"
# 最后一行设置了主节点的 ip 端口
replicaof 127.0.0.1 6379

# redis6381.conf    slave2
include /opt/redis-5.0.5/redis.conf
pidfile /var/run/redis_6381.pid
port    6381
dbfilename dump6381.rdb
logfile "my-redis-6381.log"
# 最后一行设置了主节点的 ip 端口
replicaof 127.0.0.1 6379

## 注意 redis.conf 要调整一项,设置后台运行,对咱们操作比较友好
daemonize yes

image-20200531215821358

启动节点

启动节点,然后查看节点信息

# 顺序启动节点
$ redis-server redis6379.conf
$ redis-server redis6380.conf
$ redis-server redis6381.conf

# 进入redis 客户端,开多个窗口查看方便些
$ redis-cli -p 6379
$ info replication

info replication 命令可以查看连接该数据库的其它库的信息,可看到有两个 slave 连接到 master

主节点信息

从节点信息

数据同步验证

在 master 节点设置值,在 slave1/slave2 节点可以查看数据同步情况

# master
$ redis-cli -p 6379
127.0.0.1:6379> set k1 v1
OK

# slave1
$ redis-cli -p 6380
127.0.0.1:6380> get k1
"v1"

Sentinel(哨兵)模式

上面也说了哨兵其实主动复制的自动版,所以需要先配置好主从复制,不同点在于要增加几个哨兵进行监控。

主要有两步:

  • 准备主从复制集群,并启动
  • 增加哨兵配置,启动验证
集群规划

一般来说,哨兵模式的集群是:一主,二从,三哨兵。

那咱们就来演示一下三个哨兵的集群。

节点配置端口
masterredis6379.conf6379
slave1redis6380.conf6380
slave2redis6381.conf6381
sentinel1sentinel1.conf26379
sentinel2sentinel2.conf26380
sentinel3sentinel3.conf26381
哨兵配置

哨兵的配置其实跟 redis.conf 有点像,可以看一下自带的 sentinel.conf

这里咱们创建三个哨兵文件, 哨兵文件的区别在于启动端口不同

# 文件内容
# sentinel1.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 1
# sentinel2.conf
port 26380
sentinel monitor mymaster 127.0.0.1 6379 1
# sentinel3.conf
port 26381
sentinel monitor mymaster 127.0.0.1 6379 1

image-20200531225859175

启动哨兵

先把 master-slave 启动!

然后,挨个把三个都启动了

$ redis-sentinel sentinel1.conf
$ redis-sentinel sentinel2.conf
$ redis-sentinel sentinel3.conf

启动之后日志如下,可以看到监听到的主/从节点情况以及哨兵集群情况

image-20200531230243940

主节点下线模拟

我们在 master(6379) 节点 执行 shutdown,然后观察哨兵会帮我做什么?

可以看到哨兵扫描到了 master 下线, 然后经过一系列判断,投票等操作重新选举了master(6381) 节点

image-20200531230641149

可以查看到,6381 已成为 master

image-20200531231015090

然后我们可以看到, 即使我们把原 master 节点恢复运行, 它也只是 slave 身份了存在了, 失去了大哥的身份, 可谓是风水轮流转了

image-20200531231120269

Cluster 集群模式

Redis 的 Cluster 集群模式, 启动还挺简单

主要有两步

  • 配置文件
  • 启动验证
集群规划

根据官方推荐,集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。

节点配置端口
cluster-master1redis7001.conf7001
cluster-master2redis7002.conf7002
cluster-master3redis7003.conf7003
cluster-slave1redis7004.conf7004
cluster-slave2redis7006.conf7005
cluster-slave3redis7006.conf7006
配置文件

咱们准备 6 个配置文件 ,端口 7001,7002,7003,7004,7005,7006

分别命名成 redis7001.conf ......redis7006.conf

redis7001.conf 配置文件内容如下(记得复制6份并替换端口号)

# 端口
port 7001  
# 启用集群模式
cluster-enabled yes 
# 根据你启用的节点来命名,最好和端口保持一致,这个是用来保存其他节点的名称,状态等信息的
cluster-config-file nodes_7001.conf 
# 超时时间
cluster-node-timeout 5000
appendonly yes
# 后台运行
daemonize yes
# 非保护模式
protected-mode no 
pidfile  /var/run/redis_7001.pid
启动 redis 节点
  • 挨个启动节点
redis-server redis7001.conf
...
redis-server redis7006.conf

看以下启动情况

image-20200601002803562

  • 启动集群
# 执行命令
# --cluster-replicas 1 命令的意思是创建master的时候同时创建一个slave

$ redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluste    r-replicas 1
# 执行成功结果如下
# 我们可以看到 7001,7002,7003 成为了 master 节点,
# 分别占用了 slot [0-5460],[5461-10922],[10923-16383]
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7005 to 127.0.0.1:7001
Adding replica 127.0.0.1:7006 to 127.0.0.1:7002
Adding replica 127.0.0.1:7004 to 127.0.0.1:7003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 0313641a28e42014a48cdaee47352ce88a2ae083 127.0.0.1:7001
   slots:[0-5460] (5461 slots) master
M: 4ada3ff1b6dbbe57e7ba94fe2a1ab4a22451998e 127.0.0.1:7002
   slots:[5461-10922] (5462 slots) master
M: 719b2f9daefb888f637c5dc4afa2768736241f74 127.0.0.1:7003
   slots:[10923-16383] (5461 slots) master
S: 987b3b816d3d1bb07e6c801c5048b0ed626766d4 127.0.0.1:7004
   replicates 4ada3ff1b6dbbe57e7ba94fe2a1ab4a22451998e
S: a876e977fc2ff9f18765a89c12fbd2c5b5b1f3bf 127.0.0.1:7005
   replicates 719b2f9daefb888f637c5dc4afa2768736241f74
S: ac8d6c4067dec795168ca705bf16efaa5f04095a 127.0.0.1:7006
   replicates 0313641a28e42014a48cdaee47352ce88a2ae083
Can I set the above configuration? (type 'yes' to accept): yes 
# 这里有个要手动输入 yes 确认的过程
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
...
>>> Performing Cluster Check (using node 127.0.0.1:7001)
M: 0313641a28e42014a48cdaee47352ce88a2ae083 127.0.0.1:7001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 4ada3ff1b6dbbe57e7ba94fe2a1ab4a22451998e 127.0.0.1:7002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: ac8d6c4067dec795168ca705bf16efaa5f04095a 127.0.0.1:7006
   slots: (0 slots) slave
   replicates 0313641a28e42014a48cdaee47352ce88a2ae083
S: a876e977fc2ff9f18765a89c12fbd2c5b5b1f3bf 127.0.0.1:7005
   slots: (0 slots) slave
   replicates 719b2f9daefb888f637c5dc4afa2768736241f74
M: 719b2f9daefb888f637c5dc4afa2768736241f74 127.0.0.1:7003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 987b3b816d3d1bb07e6c801c5048b0ed626766d4 127.0.0.1:7004
   slots: (0 slots) slave
   replicates 4ada3ff1b6dbbe57e7ba94fe2a1ab4a22451998e
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

image-20200601001049144

数据验证
# 注意 集群模式下要带参数 -c,表示集群,否则不能正常存取数据!!!
[root@localhost redis-5.0.5]# redis-cli -p 7100 -c
# 设置 k1 v1
127.0.0.1:7001> set k1 v1
-> Redirected to slot [12706] located at 127.0.0.1:7003
OK
# 这可以看到集群的特点:把数据存到计算得出的 slot,这里还自动跳到了 7003
127.0.0.1:7003> get k1
"v1"

# 我们还回到 7001 获取 k1 试试
[root@localhost redis-5.0.5]# redis-cli -p 7001 -c
127.0.0.1:7001> get k1
-> Redirected to slot [12706] located at 127.0.0.1:7003
"v1"
# 我们可以看到重定向的过程
127.0.0.1:7003> 

TODO ?

Docker 版 ?

也许会弄

Reference

B站-尚硅谷周阳老师的视频课--推荐新手看一看,对实操有帮助

Redis ==> 集群的三种模式

Redis哨兵(Sentinel)模式

redis cluster集群模式总结


到这里关于 Redis 的集群模式就了解的差不多了,完结,撒花 ~

不要只看不敲哦,只看或者只收藏不敲等于耍流氓 !

查看原文

赞 0 收藏 0 评论 0

猴哥一一 发布了文章 · 5月25日

[GitHub] 跟我一起白嫖 GitHub Pages 做个人站点 ?

What`s The GitHub Pages ?

Websites for you and your projects.
Hosted directly from your GitHub repository. Just edit, push, and your changes are live.

简单的说,就是提供了用 GitHub 的仓库做站点的一种方式,我们无需自己提供服务器。

而我们用 GitHub Pages 就是想白嫖啦,借助它提供的直接访问静态资源的能力,

我们可通过 GitHub Pages 功能去托管:

个人博客,

项目说明,

XX产品说明书等等

如何使用 GitHub Pages

首先你要创建自己的 GitHub 账号,点击注册

关于 Git 命令的使用,可以参考这篇文章进行学习

创建仓库

在 GitHub 创建一个名为 username.github.io 的新仓库,其中username是你在GitHub上的用户名(或组织名称)。 如果仓库的第一部分与你的用户名不完全匹配,则它将无法正常工作,因此请确保正确输入。

请注意我这里的仓库名,和我的用户名

创建仓库

初始化仓库

向仓库添加咱们的静态资源。

# git 命令行,克隆到本地,注意使用你自己的地址哦!!!
git clone https://github.com/username/username.github.io.git
# 进入文件夹
cd username.github.io
# 制作页面,注意名称要是index.html
echo "Hello World" > index.html
# 添加到暂存区
git add --all
# commit 
git commit -m "Initial commit"
# push 到远程
git push -u origin master

操作过程

访问站点

然后我们可以在浏览器输入仓库名称即可访问了,默认会访问到咱们的 index.html

访问站点

到这里,咱们已经把 GitHub Pages 用起来了,后面具体往站点上放什么资源,就可以自由 DIY 啦。

很多同学都会用 username.github.io 这个根站点维护个人博客,你也可以的。


干货赠送:GitHub Pages 其实不止这么多

可能一般人介绍 GitHub Pages 介绍到这就已经结束了,完结撒花~

GitHub Pages 就只能用 username.github.io 这一个站点 ?

No!

你没听错,你还能使用二级目录访问,创建很多仓库,每个仓库都可以做站点。

访问方式 username.github.io/repositoryName

我这里有用 vue 做了个简易的在线播放器,可以去访问体验一下,如果你喜欢直接拿走吧。。。暂时咱不讨论这个播放器怎么实现的,感兴趣的话留言交流

项目地址:

https://yuansaysay.github.io/vueMusicPlayer

代码地址:

源代码地址

https://github.com/yuansaysay/vueMusicPlayer

二级站点搞起来

简单说下怎么玩起来,

  • 创建仓库,和上面一样的方式
  • 初始化仓库,添加你需要的静态资源,比如我上面的音乐播放器代码

如果这时候你直接访问会是这个结果,因为我们还要把仓库的 Github Pages 功能开启。

404

需要启用仓库的 GitHub Pages 功能:找到setting,往下拉找到GitHub Pages,如图设置就OK !

开启仓库 GitHub Pages功能

然后就可以以复制的方式,做出各种各样的站点啦!


希望对你有帮助,有疑问可以留言交流。

开启你的 GitHub Pages 使用之旅吧!

查看原文

赞 1 收藏 1 评论 0

猴哥一一 发布了文章 · 5月24日

[Git] Git 可以这么学

Git 就这么简单

Git 命令我们可能工作中会经常使用,确实要好好总结一下。

总结下来发现,其实 Git 没那么难,了解下原理,再加上日常实战,其实就查不多了。

Git 是什么

首先 Git 是一款版本控制工具。

那啥是版本控制呢?

版本控制(Revision control)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。

主流的版本控制管理工具有 SVN,Git,CVS 等。

可以说 Git 是最先进的分布式版本控制系统(没有之一)。

Git 工作流原理【重要】

先看张图,这图还挺重要,吃透这张图,其实日常使用 Git 就没什么问题了。

这张原理图涉及到6个命令非常重要,先学会这几个,其余的其实还有百十个也记不住,用到再去学。

Git 原理

第一次看这图的话,建议从右边往左看。

解释下几个名词

  • Workspace:工作区,其实就是咱们写代码的地方
  • Index / Stage:暂存区,执行git add 命令就把工作区内容提交到了暂存区
  • Repository:仓库区(或本地仓库),执行git commit命令就会把暂存区的内容提交到本地仓库
  • Remote:远程仓库,执行git push命令就可以把本地代码推到远程分支

其实咱们的数据就是在上述几个地方流转

再结合这两个图理解一下

操作流

常用操作图示

文件状态流转【重要】

上面介绍了文件在不同区域的流转,咱们还需要了解一下文件本身的状态,以及不同命令对文件状态的影响。理解这几个状态直接的流转,有助于看清 Git 本质。

  • 没有被add过的文件叫untracked
  • add之后文件处于staged状态等待commite
  • commit之后文件处于unmodified这里之所以是modified是因为文件会跟仓库中的文件对比
  • 当unmodified的文件被修改则会变为modified状态
  • modified之后的文件add之后将继续变为staged状态
  • unmodifed的文件还有一种可能是已经不再需要了,那么可以remove它不再追踪变为untracked状态

文件状态流转

文件操作初体验

结合文件状态流转图,实践一下下面的基础文件操作命令吧。

git init 初始化git生成git仓库
git status 查看git状态
git add <filename>添文件到暂存区
git add .加入所有文件到暂存区
git commite -m 'message'提交文件到本地仓库
git reset <filename>将尚没有commite之前加入到暂存区的文件重新拉回

Git 常用命令

初始化仓库

首先咱们得有个工作区域对不对

# case1
# 在当前目录创建一个文件夹
$ mkdir [project-name]
# 在当前目录新建一个Git代码库
$ git init
# case2
# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]
# case3
# 下载一个项目和它的整个代码历史(各个分支提交记录等)
$ git clone [url]
注意:git init 会产生 .git 文件夹,windows默认看不到,需要设置一下显示隐藏文件才能看到

增加/删除文件

结合上面的原理图,去理解常用的 add commit 命令

# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

# 提交暂存区到仓库区
$ git commit -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

# 提交更改到远程仓库
$ git push origin master

# 拉取远程更改到本地仓库默认自动合并
$ git pull origin master

查看信息

我们可以看当前 git 状态,提交日志,文件差异等待内容

# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

标签

标签其实是一种版本的概念,发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。

commit号一般长这样6a5819e...,比如你要回退到这个版本,你记得住6a5819e...吗?这时候标签就起作用了。

# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

分支管理

分支其实是一种并行开发(协作)的方式,可以在不同的分支上干各自是事。

# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

撤销(俗称后悔药)

咱们干活不是只能往前走,偶尔我们可能会需要回到之前的版本,那这个时候撤销命令就上场了

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

干货:常见疑问

你认为 Git 最重要的是什么?

个人认为是 commit ,所有的概念都是围绕它展开的,

比如分支其实就是 commit 的集合载体,

而在执行 merge 的时候,也是针对 commit 进行合并,

在 reset 的时候,也是回退到某个 commit,

所以理解 commit 对于整个 Git 工作流非常重要。

git reset --soft --hard --mixed 的区别

可以通过 git reset -help 查看官方说明,其实说的挺明白。

为什么说的这么明白了,好像还很多人(包括我)有疑问呢?估计是被网文影响的吧,或者像我一样压根没看过...

--mixed               reset HEAD and index    # 只改变HEAD指针和暂存区
--soft                reset only HEAD        # 只改变HEAD
--hard                reset HEAD, index and working tree#改变 HEAD/index/workspace

# 补充一下,
reset 操作实际上改变HEAD(本地仓库)指针指向的commit,index 就是指的咱们说的暂存区。
从实际执行结果来看,mixed和soft都不会改变本地代码,只有hard方式会改变本地代码
# 注意一下:soft 和 mixed 操作结果的不同点
soft:导致文件的状态处于 staged 状态
mixed:导致文件的状态处于 modified 状态
结合上面的**文件状态流转图**理解一下

衍生的问题

如果遇到某些命令不清楚用法,可以 git 某命令 -help,去看官方输出,其实最直接了。

有些东西被网上各种转义反而会让人产生疑惑。

# 比如上面的 reset 命令
$ git reset -help

usage: git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]
   or: git reset [-q] [<tree-ish>] [--] <paths>...
   or: EXPERIMENTAL: git reset [-q] [--stdin [-z]] [<tree-ish>]
   or: git reset --patch [<tree-ish>] [--] [<paths>...]

    -q, --quiet           be quiet, only report errors
    --mixed               reset HEAD and index
    --soft                reset only HEAD
    --hard                reset HEAD, index and working tree
    --merge               reset HEAD, index and working tree
    --keep                reset HEAD but keep local changes
    --recurse-submodules[=<reset>]
                          control recursive updating of submodules
    -p, --patch           select hunks interactively
    -N, --intent-to-add   record only the fact that removed paths will be added later
    -z                    EXPERIMENTAL: paths are separated with NUL character
    --stdin               EXPERIMENTAL: read paths from <stdin>

命令太多记不住怎么办?

计算机行业工作多数需要实操,程序员是一个熟练工种。

Git 是咱们的一个工具,一般咱们能熟练使用就行,并不需要成为使用专家。

当然一件事如果是重要的且频繁要做的,用的多了,自然就记住了。
否则,记不住就记不住吧,能找到怎么用就行

References

阮一峰老师的Git命令清单

廖雪峰老师的Git教程

全面理解Git

查看原文

赞 0 收藏 0 评论 0

猴哥一一 发布了文章 · 5月17日

Test Send

test content
hello world
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 5 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-23
个人主页被 84 人浏览