【SpringCloud 系列】Eureka 注册中心初体验
在 SpringCloud 微服务体系中,有几个比较重要的组件,如注册中心,配置中心,网关,安全、负载均衡、监控等等,接下来我们将来看一下这些常用的组件有什么用,在微服务架构下的该怎么用。本文为为第一篇,注册中心 Eureka 的使用说明I. 基本介绍1. 注册中心注册中心,主要的核心点是服务的注册与发现。简单来讲,就是我们的所有服务都会在注册中心上标识自己,注册中心统一管理所有的服务名与具体的应用之间的映射关系,这样微服务之间的访问,就可以直接通过服务名来相互通信,相比较于直接通过 ip 端口的访问,这样的好处是当某个服务下线、新增或者换了机器,对调用者而言,只要维持一份注册中心的最新映射表即可,不需要其他任何改动逻辑。我们通常可用的注册中心有 Eureka, Consul, Zookeeper, nacos等,在我们后续的教程中会逐一进行介绍Eureka2.x 闭源,1.x 虽然可用,但新项目的话不建议再使用它,比如Consul, nacos 都是不错的选择如果出于学习的目的,或者由于历史原因(比如我),学习了解一下 Eureka 知识点也没什么坏处2. EurekaEureka 是 Netflix 开源的服务发现组件,本身是一个基于 REST 的服务,通常包含 Server 和 Client 端原理如下图server: 提供服务注册,并在服务注册表中存储所有可用服务节点的信息client: 简化与 Server 之间的交互,比如封装了发送心跳,获取注册信息表等基本操作II. 实例演示1. 版本说明后续的演示项目中,我们的环境与版本信息如下开发环境: IDEA + mavenSpringBoot: 2.2.1.RELEASESpringCloud: Hoxton.M22. Eureka Server 端Eureka 区分了 Server 和 Client 两端,即我们有一个独立的注册中心服务,其他的微服务则作为 Client 端Server 端核心依赖如下<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
复制代码然后在配置文件中,添加一些基本信息server:
port: 8081 #服务注册中心端口号
eureka:
instance:
hostname: 127.0.0.1 #服务注册中心IP地址
client:
registerWithEureka: false #是否向服务注册中心注册自己
fetchRegistry: false #是否检索服务
serviceUrl: #服务注册中心的配置内容,指定服务注册中心的位置
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
复制代码请注意,上面的registerWithEureka这个配置,设置为 false,不像自己注册服务(后续会介绍多个 Eureka 实例时,可以如何配置)然后再启动类上,添加注解@EnableEurekaServer来申明 Eureka 服务@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码到此,一个 Eureka 服务端已经完成,此时我们可以直接访问http://localhost:8081,会看到一个自带的控制台,会提供一些基本信息3. Eureka 客户端我们这里设计两个客户端,一个提供服务,另外一个调用,演示一下 Eureka 的基本功能a. 客户端 eureka-service-provider客户端需要在 pom 文件中,添加下面的关键依赖<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
复制代码接下来需要在配置文件中,指定注册中心的地址,以及服务名(请注意,这个服务名是重要线索,后面会用到!!!)server:
port: 8082 #服务端口号
eureka:
client:
serviceUrl: #注册中心的注册地址
defaultZone: http://127.0.0.1:8081/eureka/
spring:
application:
name: eureka-service-provider #服务名称--调用的时候根据名称来调用该服务的方法
复制代码同样的需要在启动类上,通过@EnableEurekaClient来标注客户端@EnableEurekaClient
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
复制代码我们在这个项目中,写一个基本 REST 接口,供后面的服务进行调用@RestController
@RequestMapping(path = "userService")
public class UserService {
@Override
@RequestMapping(path = "/getUserById")
public UserDTO getUserById(@RequestParam long userId) {
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
userDTO.setNickname("一灰灰blog");
userDTO.setUserName("yihuihuiblog");
userDTO.setPhone(88888888L);
return userDTO;
}
}
复制代码再看一下上面的实现,你会发现和平时写的 Controller 没有任何的区别到这里第一个 Eureka 客户端已经完成,并提供了一个 REST 接口,接下来我们开始写第二个 Eureka 客户端,用来访问上面的 REST 服务b. 客户端 eureka-service-consumer基本的流程和上面没有任何区别,只是将配置文件稍微改一下server:
port: 8083 #服务端口号
eureka:
client:
serviceUrl: #注册中心的注册地址
defaultZone: http://127.0.0.1:8081/eureka/
spring:
application:
name: eureka-service-consumer #服务名称--调用的时候根据名称来调用该服务的方法
复制代码那么在这个服务中,如何访问 eureka-service-provider 提供的服务呢?通过RestTemplate来实现请注意,这个 RestTemplate 和我们普通的new RestTemplate()创建的不一样哦,我们是通过如下方式获取实例@Bean
@LoadBalanced
public RestTemplate rest() {
return new RestTemplate();
}
复制代码重点关注方法上的@LoadBalanced注解,这个会在后续的 Ribbon 的章节中深入介绍,在这里只需要知道通过它生成的RestTemplate,在发起访问时,会借助 Eureka 的注册信息表,将服务名翻译为对应的ip+端口号接下来就是我们的访问环节,写法如下@Autowired
private RestTemplate restTemplate;
@GetMapping(path = "uid")
public String getUser(int userId) {
UserDTO dto = restTemplate
.getForObject("http://eureka-service-provider/userService/getUserById?userId=" + userId, UserDTO.class);
return userId + "'s info: " + dto;
}
复制代码请着重看一下访问的 url: "http://eureka-service-provider/userService/getUserById?userId=" + userId,这里没有域名,没有 ip,是直接通过服务名进行访问的4. 测试与小结我们依次将上面的 Server 和两个 Client 启动,然后访问http://localhost:8081,查看 Eureka 控制台,可以看到如下界面,两个客户端都已经注册好了然后再测试一下通过 consumer 访问 provider 的服务到此 Eureka 的核心功能已经演示完毕,当然如果仅仅只是这样,这个学习成本好像很低了,作为一个有思考的小青年,看了上面的流程自然会有几个疑问安全问题注册中心控制台直接访问,这要是暴露出去了...一个 Eureka 实例,单点故障怎么解服务注册多久生效?服务下线多久会从注册信息表中摘除?服务存活判断是怎样的?通过RestTemplate方式使用,很不优雅啊,有没有类似 rmi 的通过类调用的方式呢?
Git高效实践(上)
一、从项目说起我们从上一章用Maven搭建的项目讲起,老样子怎么安装Git不说,前面结合IDEA讲解操作,后面都是基于命令行的教程,在终端输入git --version输出版本表示已经安装成功:$ git --versiongit version 2.17.0以后表示输入命令都使用$开头1. 添加配置# 配置用户
$ git config --global user.name "AiJiangnan"
# 配置用户邮箱
$ git config --global user.email "904629998@qq.com"
# 查看配置
$ git config -l带有--global 参数的是全局配置2. 创建仓库使用IDEA打开项目,点击菜单:VCS -> Enable Version Control Integration,弹出成功框后当前项目就已经创建好本地仓库了。如果没有成功,检查IDEA设置是否集成了Git。打开设置(Ctrl+Alt+S):File -> Settings -> Version Control -> Git 进行配置:在第一项设置 Path to Git executable 配置Git的执行文件路径,点击右侧的 Test 弹出成功框,表示配置成功。配置成功后再执行一下前面创建仓库的操作即可。打开IDEA的版本管理界面(Alt+9)如下:在左侧菜单图标找到 Group By 勾选 Directory 就可以将修改的文件以目录的形式显示。4. 提交代码发现项目中的文件并不是所有的文件都要交给Git仓库管理,我们只需要管理源代码和必要的资源文件,像IDEA的配置文件和编译生成的文件都可以忽略,在项目根目录创建一个.gitignore文件,并输入需要忽略的文件或目录。下面是参考内容:# idea
.idea
*.iml
# maven
target
# java
*.class
*.log此时 Local Changes 中那些忽略的文件和文件夹已经没有了。全选中 Local Changes 中的文件右击,然后选择 Commit (Ctrl+K),弹出下面窗口:左侧上方显示选中的提交的文件,下方填写本次修改的信息,为了记录每一次提交都做了什么改变。右侧是配置一些在提交前和提交后的操作,比如在提交前(Before Commit)我们可以:Reformat code:格式化代码Optimize imports:优化导包Check TODO(Show All):检查TODO配置自己想要的操作后单击 Commit (Ctrl+Enter)完成提交。此时在 Log 界面可以看到这次提交记录。5. 分支管理单击IDEA状态栏右下角的 Git: master 然后选择 New Branch,创建一个新分支,取名为dev为我们的开发分支。单击IDEA状态栏右下角的 Git: dev -> master -> Checkout,检出主分支。对分支的操作都可以在这进行,比如我们常用的操作:Checkout:检出选中分支Checkout As...:从选中分支创建新分支Compare With...:当前分支与选中分支比较Merge into Current:合并选中分支到当前分支Rename...:重命名选中分支Delete:删除选中分支6. 解决冲突 当不同的分支上编辑了同一个文件时,必定会产生冲突,我们在合并的时候需要解决冲突,在IDEA里面解决冲突很简单,在master和dev分支上同时修改.gitignore文件,然后将dev合并到主分支上:操作:Accept Yours:接受当前分支的修改Accept Theirs:接受选择分支的修改Merge...:合并操作选择 Merge... 操作后:Accept Left 和 Accept Right 的作用是选择接受哪一方的全部修改。点击冲突区域的 << 和 >> 完成合并操作点击 Apply ,整个解决冲突的过程结束。7. 推送代码远程仓库不管是使用Github、Gitlab还是码云,操作都类似,在这拿Github举例。添加SSH公钥查看之前是否生成过SSH公钥:$ cd ~/.ssh/ | ls
# 公钥文件: id_rsa.pub没有的自己生成,输入下面命令,然后一路回车:$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
Generating public/private rsa key pair.
Enter a file in which to save the key (/home/you/.ssh/id_rsa): [Press enter]生成后把公钥文件id_rsa.pub里的内容复制下来。# 查看公钥文件内容
$ cat ~/.ssh/id_rsa.pub注册并登录Github,在首页点击头像下拉菜单,点击:Settings -> SSH and GPG keys -> New SSH key,输入 Title ,把前面复制的公钥输入到 Key 输入框中,点击 Add SSH key 完成。新建并绑定远程仓库在网站上新建一个仓库 Repository ,复制下它的地址:git@github.com:AiJiangnan/spring-boot.git,注意确保协议选择SSH。IDEA进入Git操作的方式:菜单:VCS -> Git右击项目 -> Git右击打开的文件 -> Git右击项目或文件方式进入时,当IDEA打开多个仓库时可以区分仓库在IDEA中给当前项目添加远程仓库:Git -> Repository -> Remotes...,点击 + 号添加远程仓库,需要添加 Name 和 URL ,默认远程仓库名称为origin,将前面复制远程仓库地址粘贴到 URL 栏中点击 OK 完成操作。Git支持的协议:LocalHTTPSSHGit推送代码打开推送操作:Git -> Repository -> Push... (Ctrl+Shift+K)左侧展示将要推送的历史提交,可以修改推送的远程仓库和远程分支。右侧展示每次提交修改的文件,可以查看每个文件的Diff。下面 Push Tags 设置是否要推送标签,选择推送哪个标签。配置好后点击 Push (Ctrl+Enter) 完成操作。我们在后面代码的修改中和版本的维护中都会重复下列的操作:Commit:提交Pull:拉取(更新)代码,有冲突情况Merge:合并,有冲突情况Push:推送,有冲突情况,确保Push前执行Pull不建议存在冲突。Reset:回滚Git的操作远不止如此,后面我们会基于命令行模式深入讲解Git,掌握了命令行操作,再回过头来使用IDEA就感觉很轻松了,对代码的掌控能力将会大大提升。二、GitGit是一个分布式版本管理软件,不需要服务端的支持就能完成工作,基于仓库设计思想,每次提交的版本直接记录快照,而不是记录差异。Git 更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。后面讲解如何配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)或提交(commit)更改。本章也将向你演示如何配置 Git 来忽略指定的文件和文件模式、如何迅速而简单地撤销错误操作、如何浏览你的项目的历史版本以及不同提交(commits)间的差异、如何向你的远程仓库推送(push)以及如何从你的远程仓库拉取(pull)文件。1. 版本管理在进行后面的操作,我们要提前做一些前面讲到的配置,如果已配置,请忽略:# 配置用户
$ git config --global user.name "AiJiangnan"
# 配置用户邮箱
$ git config --global user.email "904629998@qq.com"
# 查看配置
$ git config -l1.1 创建仓库1.1.1 在现有目录中初始化仓库进入目录执行下面命令:$ git init执行后会在目录下创建一个.git的子目录,这个目录包含所有初始化的Git仓库所有必须的文件。1.1.2 克隆已有的仓库已有的仓库地址或者Github上的地址:git@github.com:AiJiangnan/spring-boot.git$ git clone git@github.com:AiJiangnan/spring-boot.git你想从公司远程仓库克隆项目或者想为某个开源项贡献自己的一份力,就可以用此命令创建仓库。1.2 提交代码1.2.1 文件的生命周期在工作目录下文件的状态:未跟踪(untracked):没有纳入版本控制的文件已跟踪(tracked):已经纳入版本控制的文件未修改(unmodified):没有修改的文件已修改(modified):已经修改的文件已暂存(staged):已经放入暂存区的文件已跟踪文件在上一次快照中有它们的记录,工作一段时间后,它们的状态可以是未修改、已修改或已暂存。未跟踪文件既不存在于快照记录中,也没有放入暂存区。克隆方式创建的仓库,工作目录中所有文件都是已跟踪文件,并处于未修改状态。编辑过某些文件之后,由于自上次提交你对它们做了修改,Git将它们标记为已修改文件。我们逐步将这些修改过的文件放入暂存区,然后提交所有暂存了的修改,如此反复。使用 Git 时文件的生命周期如下:1.2.2 检查当前文件状态创建一个目录并通过前面讲的git init初始化仓库,在当前仓库下操作。# 初始化仓库
$ git init
已初始化空的 Git 仓库于 /home/ajn/Code/csdn/git/.git/
# 查看哪些文件处于什么状态
$ git status
位于分支 master
尚无提交
无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪)从上面命令返回结果我们知道当前没有提交,创建一个README.txt文件:$ echo 'Hello world' > README.txt
$ git status
位于分支 master
尚无提交
未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)
README.txt
提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)返回结果为当前没有提交,并且存在未跟踪的文件README.txt,未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件,Git 不会自动将之纳入跟踪范围,除非你手动添加跟踪,所以你不必担心不想被跟踪的文件包含进来。1.2.3 跟踪新文件# 跟踪新文件
$ git add README.txt
$ git status
位于分支 master
尚无提交
要提交的变更:
(使用 "git rm --cached <文件>..." 以取消暂存)
新文件: README.txt从返回结果我们可知,该文件已跟踪,并且处于已暂存状态。此时提交,该文件此时此刻的版本就会被保留在历史记录中。git add后面参数可以是文件或目录,如果是目录,会递归跟踪该目录下所有的文件。1.2.4 暂存已修改文件如果你修改了一个已跟踪的文件CONTRIBUTING.txt,然后查看状态:$ git status
位于分支 master
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
新文件: README.txt
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)
修改: CONTRIBUTING.txt从结果可知,说明已跟踪文件的内容发生了变化,但还没有放到暂存区。要暂存这次更新,需要运行git add命令。这是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。将这个命令理解为“添加内容到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。现在让我们运行git add将CONTRIBUTING.txt放到暂存区,然后再看看git status的输出:$ git add CONTRIBUTING.txt
$ git status
位于分支 master
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
修改: CONTRIBUTING.txt
新文件: README.txt现在两个文件都已暂存,下次提交时就会一并记录到仓库。假设此时,你想要再修改 CONTRIBUTING.txt 里的内容,重新编辑保存后,准备好提交。再运行 git status 看看:$ vim CONTRIBUTING.txt
$ git status
位于分支 master
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
修改: CONTRIBUTING.txt
新文件: README.txt
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)
修改: CONTRIBUTING.txt现在CONTRIBUTING.txt文件同时出现在暂存区和非暂存区。实际上Git只不过暂存了你运行git add命令时的版本,如果你现在提交,CONTRIBUTING.txt的版本是你最后一次运行git add命令时的那个版本,而不是你运行git commit时,在工作目录中的当前版本。所以,运行了git add之后又作了修订的文件,需要重新运行git add把最新版本重新暂存起来:$ git add CONTRIBUTING.txt
$ git status
位于分支 master
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
修改: CONTRIBUTING.txt
新文件: README.txtgit add使用参数-u可以只暂存已跟踪的文件。1.2.5 状态简览# git status -s 或 git status --short
$ git status -s
M CONTRIBUTING.txt
A README.txt状态符号含义:??:未跟踪文件A:新添加到暂存区中的文件M:修改过的文件MM:左侧的表示该文件被修改并已放入暂存区,右侧表示文件被修改没放入暂存区1.2.6 忽略文件一般我们总会有些文件无需纳入Git的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。在这种情况下,我们可以创建一个名为.gitignore的文件,列出要忽略的文件模式。来看一个实际的例子:# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf文件.gitignore的格式规范如下:所有空行或者以 # 开头的行都会被忽略。可以使用标准的glob模式匹配。匹配模式可以以(/)开头防止递归。匹配模式可以以(/)结尾指定目录。要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。所谓的glob模式是指shell所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc]匹配任何一个列在方括号中的字符;问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有0到9的数字);使用两个星号(*) 表示匹配任意中间目录,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。1.2.7 查看已暂存和未暂存的修改假如再次修改README.txt文件后暂存,然后编辑CONTRIBUTING.txt文件后先不暂存,运行status命令将会看到:$ git status
位于分支 master
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
修改: README.txt
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)
修改: CONTRIBUTING.txt
# 查看尚未暂存的文件更新了哪些部分
$ git diff
diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt
index afce56f..d4ef1b9 100644
--- a/CONTRIBUTING.txt
+++ b/CONTRIBUTING.txt
@@ -1,3 +1,2 @@
Contributing
Contributing
-Contributing
# 查看已暂存的将要添加到下次提交里的内容
$ git diff --cached
diff --git a/README.txt b/README.txt
index 802992c..57bf5a7 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,2 @@
Hello world
+Hello world我们使用 git diff 来分析文件差异。如果你喜欢通过图形化的方式或其它格式输出方式的话,可以使用git difftool命令来用 Araxis ,emerge 或 vimdiff 等软件输出 diff 分析结果。使用 git difftool --tool-help 命令来看你的系统支持哪些 Git Diff 插件。1.2.8 提交更新现在的暂存区域已经准备妥当可以提交了。在此之前,确认还有什么修改过的或新建的文件还没有 git add 过,否则提交的时候不会记录这些还没暂存起来的变化。这些修改过的文件只保留在本地磁盘。所以,每次准备提交前,先用 git status 看下,是不是都已暂存起来了, 然后再运行提交命令:$ git commit
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#new file: README
#modified: CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C使用git config --global core.editor 命令设定你喜欢的编辑软件,这里用的是vim。另外,你也可以在commit命令后添加 -m 选项,将提交信息与命令放在同一行,如下所示:$ git commit -m 'update master'
[master 52a10fa] update master
2 files changed, 1 insertion(+), 1 deletion(-)1.2.9 跳过使用暂存区域$ git commit -a -m 'update master'1.2.10 移除文件$ git rm PROJECTS.md
# 取消跟踪文件,从git仓库中移除而不从硬盘上删除
$ git rm --cached PROJECTS.md1.2.11 移动文件$ git mv file_from file_to1.3 查看日志# 查看所有提交日志
$ git log
commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master)
Author: AiJiangnan <904629998@qq.com>
Date: Wed Dec 5 18:48:41 2018 +0800
update master
commit 0907ee43bf485a05492e84bf7a22c4174e2f9df3
Author: AiJiangnan <904629998@qq.com>
Date: Wed Dec 5 18:08:46 2018 +0800
update
commit f3f5b5be459610325085ad0183f330576a0742d8
Author: AiJiangnan <904629998@qq.com>
Date: Wed Dec 5 12:01:41 2018 +0800
add CONTRIBUTING.txt
# 参数:-p 查看每次提交的内容差异,-1 显示最近1次提交的日志
$ git log -p -1
commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master)
Author: AiJiangnan <904629998@qq.com>
Date: Wed Dec 5 18:48:41 2018 +0800
update master
diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt
index afce56f..d4ef1b9 100644
--- a/CONTRIBUTING.txt
+++ b/CONTRIBUTING.txt
@@ -1,3 +1,2 @@
Contributing
Contributing
-Contributing
diff --git a/README.txt b/README.txt
index 802992c..57bf5a7 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,2 @@
Hello world
+Hello world
# 参数:--stat 查看每次提交简略的统计
$ git log --stat
commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master)
Author: AiJiangnan <904629998@qq.com>
Date: Wed Dec 5 18:48:41 2018 +0800
update master
CONTRIBUTING.txt | 1 -
README.txt | 1 +
2 files changed, 1 insertion(+), 1 deletion(-)
# 参数:--pretty 格式化输出日志 默认格式:(onelone,short,full,fuller)
$ git log --pretty=oneline
52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master) update master
0907ee43bf485a05492e84bf7a22c4174e2f9df3 update
f3f5b5be459610325085ad0183f330576a0742d8 add CONTRIBUTING.txt
# 参数:--pretty 格式化输出日志 自定义格式
$ git log --pretty=format:"%h - %an, %ar : %s"
52a10fa - AiJiangnan, 2 天前 : update master
0907ee4 - AiJiangnan, 2 天前 : update
f3f5b5b - AiJiangnan, 2 天前 : add CONTRIBUTING.txt选项说明%H提交对象(commit)的完整哈希字串%h提交对象的简短哈希字串%T树对象(tree)的完整哈希字串%t树对象的简短哈希字串%P父对象(parent)的完整哈希字串%p父对象的简短哈希字串%an作者(author)的名字%ae作者的电子邮件地址%ad作者修订日期(可以用 --date= 选项定制格式)%ar作者修订日期,按多久以前的方式显示%cn提交者(committer)的名字%ce提交者的电子邮件地址%cd提交日期%cr提交日期,按多久以前的方式显示%s提交说明# 参数:--graph 使用字符图形化展示分支,合并历史
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
* 11d191e Merge branch 'defunkt' into local选项说明-p按补丁格式显示每个更新之间的差异。--stat显示每次更新的文件修改统计信息。--shortstat只显示 --stat 中最后的行数修改添加移除统计。--name-only仅在提交信息后显示已修改的文件清单。--name-status显示新增、修改、删除的文件清单。--abbrev-commit仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。--relative-date使用较短的相对时间显示(比如,“2 weeks ago”)。--graph显示 ASCII 图形表示的分支合并历史。--pretty使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。--oneline--pretty=oneline 的简写1.3.1 限制输出长度# 参数:--since 配置日志输出的起始时间
$ git log --since=2.weeks
# 参数:-S 搜索修改了某些特定内容的日志
$ git log -Sfunction_name选项说明-(n)仅显示最近的 n 条提交--since, --after仅显示指定时间之后的提交。--until, --before仅显示指定时间之前的提交。--author仅显示指定作者相关的提交。--committer仅显示指定提交者相关的提交。--grep仅显示含指定关键字的提交-S仅显示添加或移除了某个关键字的提交1.4 回滚代码1.4.1 重置原理树用途HEAD上一次提交的快照Index(索引)预期的下一次提交的快照(暂存区域)Working Directory工作目录工作流程1.4.2 撤消操作# 前一次提交中追加修改
$ git commit -m "update README.txt"
$ git add CONTRIBUTING.txt
$ git commit --amend
# 取消暂存的文件
$ git add *
$ git status -s
M CONTRIBUTING.txt
M README.txt
$ git reset HEAD CONTRIBUTING.txt
重置后取消暂存的变更:
MCONTRIBUTING.txt
$ git status -s
M CONTRIBUTING.txt
M README.txt
# 撤消对文件修改
$ git checkout -- CONTRIBUTING.txt
$ git status -s
M README.txt1.4.3 回滚操作
# 移动 HEAD (参数:--soft,回滚到commit操作前)
$ git reset --soft HEAD~
$ git status -s
M CONTRIBUTING.txt
# 更新索引 (参数:--mixed(默认),回滚到add操作前)
$ git reset --mixed HEAD~
重置后取消暂存的变更:
MCONTRIBUTING.txt
$ git status -s
M CONTRIBUTING.txt
# 更新工作目录 (参数:--hard,回滚到修改前)
$ git reset --hard HEAD~
HEAD 现在位于 52a10fa update master
$ git status -s
$ git log --oneline
52a10fa (HEAD -> master) update master
0907ee4 update
f3f5b5b add CONTRIBUTING.txt
# 回滚并将回滚的操作当作一次提交
$ git revert 0907ee4Git中任何已提交的东西都可以恢复,然而你未提交的东西丢失后可能再也找不回来了。回滚中定位版本可以用指针HEAD~或者版本哈希码0907ee4来指定,例如:前一个版本HEAD~,前两个版本HEAD~2。1.5 数据恢复删除了正在工作的分支回滚了你想要的提交当你遇到上面两种情况:
# 查看日志并回滚到前三次提交
$ git log --oneline
ac705e4 (HEAD -> master) update
4f9a890 update
52a10fa update master
0907ee4 update
f3f5b5b add CONTRIBUTING.txt
$ git reset --hard 0907ee4
HEAD 现在位于 0907ee4 update
$ git log --oneline
0907ee4 (HEAD -> master) update
f3f5b5b add CONTRIBUTING.txt
# 查看操作日志恢复数据版本
$ git reflog -5
0907ee4 (HEAD -> master) HEAD@{0}: reset: moving to 0907ee4
ac705e4 HEAD@{1}: commit: update
4f9a890 HEAD@{2}: reset: moving to 4f9a890
52a10fa HEAD@{3}: reset: moving to 52a10fa
52a10fa HEAD@{4}: checkout: moving from dev to master
$ git reset --hard ac705e4
HEAD 现在位于 ac705e4 update
$ git log --oneline
ac705e4 (HEAD -> master) update
4f9a890 update
52a10fa update master
0907ee4 update
f3f5b5b add CONTRIBUTING.txt
漫谈!如何简单明了通过分解和增量更改将单体迁移到微服务
本文要点微服务迁移不是一个小更改。你必须搞清楚它是否真的能解决你的问题,否则你可能会创建一个会杀死你的、乱糟糟的实体。单体有不同类型,其中一些可能是有效的,足以满足业务需求。单体不是一个应该被杀死的敌人。微服务关乎独立部署。有一些分解和增量更改模式可以帮助你评估并迁移到微服务架构。当你开始使用微服务时,你会意识到随之而来的是一系列非常复杂的挑战。所以不应该将微服务作为默认选择。你得仔细考虑它们是否适合你。在伦敦 QCon 大会上,我谈到了 单体分解模式以及我们如何达成微服务 。我喜欢把它们比作令人讨厌的水母,因为它们是一种乱糟糟的实体,会刺痛甚至可能杀死我们。这在通常的企业微服务迁移中很常见。许多组织正在经历某种数字化转型。随便看下当前的任何数字化转型,我们都会发现微服务的身影。我们知道,数字化转型是一件大事,因为现在任何机场候机室都有大型 IT 咨询公司的广告推销数字化转型,包括德勤、DXC、埃森哲等公司。微服务非常流行。不过,在谈及微服务时,我关注的是结果,而不是我们用来实现它们的技术。我们选择微服务架构的原因有很多,但我反复提到的一个原因是其独立部署的属性。有一个功能,一个我们想要改变系统行为的更改。我们想要尽快实现这个更改。图 1:微服务方法示意图将微服务架构与单体做下比较。我们认为,单体是一个单一的、无法透视地块,我们无法对它作出任何更改。单体被认为是我们生活中最糟糕的东西,是难以摆脱的沉重负担。我认为这非常不公平。最终,“单体”一词在过去两三年里取代了我们之前使用的“遗留问题(legacy)”一词。这是一个根本性问题,因为有些人开始将单体视为遗留问题,是需要移除的东西。我认为这非常不合适。单体的类型单体有多种形式和规模。在讨论单体应用程序时,我主要是将单体作为部署单元来讨论。考虑下经典的单体,它是将所有代码打包在单个进程中。它可能是 Tomcat 中的一个 WAR 文件,也可能是一个基于 PHP 的应用程序,所有代码都打包在一个可部署单元中,该单元会与数据库通信。这种单体类型可以看作是一个简单的分布式系统。分布式系统是由多台通过非本地网络相互通信的计算机组成的系统。在这种情况下,所有的代码都打包在一个进程中,重要的是,所有数据都保存在于一个运行在不同机器上的大型数据库中。把所有数据都放在一个数据库中,将来会给我们带来很多痛苦。图 2:模块化单体我们还可以考虑下单进程单体的一种变体,称为模块化单体。这种模块化单体使用了关于结构化编程的前沿思想(诞生于 20 世纪 70 年代初,几十年后,我们中的一些人仍在努力掌握这些思想!)。如图 2 所示,我们将单进程单体应用程序分解为模块。如果我们正确地划分了模块边界,我们就可以独立地处理每个模块。但是,本质上,部署过程仍然是静态链接的方法:我们必须链接好所有模块才能进行部署。比如一个 Ruby 应用程序,它有许多 GEM 文件、NuGet 包括通过 Maven 组装的 JAR 文件组成。虽然我们仍然是单体部署,但模块化单体有一些显著的好处。把代码分解成模块确实可以让我们在一定程度上独立地完成工作。它可以方便不同的团队一起工作,并处理系统的不同方面。我认为这是一个被严重低估的选项。这其中存在的问题是,人们往往不善于定义模块边界——更确切地说,即使他们擅长定义模块边界,他们也不擅长保持这些边界。遗憾的是,结构化编程或模块化的概念往往会遭遇“泥球”问题。对于我服务过的许多组织来说,使用模块化单体比使用微服务架构会更好。在过去的三年里,我对我一半的客户说过:“微服务不适合你。“有些客户甚至听了我的话。对于它们中的许多来说,一种可以定义模块边界的好方法就足以满足它们的需要。他们可以得到一个比较简单的分布式系统,以及一定程度上独立、自主地工作。图 3:模块化单体的一种变体模块化单体也有变体。图 3 看起来有点奇怪,但这是我多次提出的建议,特别是对于初创公司,我通常认为,他们最好不要着急上微服务。如图 3 所示,我们使用了模块化单体,并将后台单个的整体数据库进行了分解,这样就可以单独存储和管理每个模块的数据。虽然这看起来很奇怪,但归根结底这是一种对冲架构。人们认识到,分解单体架构时最困难的工作之一是处理数据层。如果我们能提前设计好与这些模块相关联的独立数据库,以后迁移到单独的微服务就会更容易。如果我正在处理模块 C,我对与模块 C 关联的数据具有完全的所有权和控制权。当模块 C 变成一个单独的服务时,迁移它应该会更容易。当我还在 ThoughtWorks 工作时,我的一位老同事 Peter Gillard-Moss 第一次向我展示了这种模式。这是他为我们正在开发的一个内部系统设计的。他说,“我觉得这能行。我们不确定我们是否想要提供服务,所以也许它应该是一个单体。”我说,“试一试。看看会发生什么。“大约 6 年过去了,去年我和 Peter 谈过,ThoughtWorks 仍然没有改变架构。它仍然运行得很欢快。他们让不同的人处理不同的模块,即使是在这个级别上将数据分离开来,也给他们带来了巨大的好处。图 4:分布式单体现在,我们来看看最糟糕的单体——分布式单体。我们的应用程序代码现在运行在彼此通信的独立进程上。不管出于什么原因,我们都必须将整个系统作为一个单元同步部署。经常,这种情况的出现是因为我们弄错了服务边界。我们将业务逻辑胡乱地放在了不同的层上。我们没有遵从关于耦合和内聚的要点,现在,我们的结账逻辑分布在服务栈中 15 个不同的地方。我们要做任何工作,都必须协调多个团队。如果组织中存在大量的横切更改,通常表明组织边界或服务边界定义的不对。分布式单体的问题在于,它本质上是一个更加分布式的系统,但是对于所有相关的设计、运行和操作挑战,我们仍然需要单体需要的那些协调活动。我想在线部署,但我不能。我必须等你完成更改,但你也完不成,因为你在等别人。现在,我们一致同意:“好吧,7 月 5 日,我们将一起上线。每个人都准备好了吗?三、二、一,部署。“当然,一切都很顺利。对于这类系统,我们从来没有遇到过任何问题。如果一个组织有一位全职的发布协调经理或这方面的其他职位,那么他们可能有一个分布式单体。协调分布式系统的同步部署一点都不好玩。我们最终会付出更高的更改成本。部署的范围会大很多,可能会有更多的地方出错。这种难以避免的协调活动,不只存在于发布活动中,而且存在于一般部署活动中。稍微看下精益生产的内容就会发现,减少交接是优化生产力的关键。等待别人为我做点什么只会产生浪费。这会导致生产力瓶颈。为了更快地交付软件,减少交接和协调非常关键。遗憾的是,分布式单体往往会创造出不得不进行协调的环境。有时,我们的问题不在于服务边界在哪里。有时,它完全是始于我们开发软件的方式。有些人从根本上误解了发布序列。发布序列一直被认为是一种治疗性的发布技术,而不是一种进取性的活动。我们会选择像发布序列这样的东西来帮助组织转向持续交付。发布序列的概念以定期为基础,也许每四周,软件的所有部分已经准备就绪。如果软件还没有准备好,它就会被推迟到下一个发布序列。对许多组织来说,这是向前迈出了一步。我们应该缩小发布序列之间的间隔,最终完全消除。然而,有太多的组织在采用了发布序列就再也没有继续前进。当若干团队都朝着同一个发布序列而努力时,所有已经准备好的软件都会在这个发布序列中交付——突然之间,我们会一次性部署大量的服务。这是真正的问题所在。当实践发布序列时,最重要的一件事是,至少要将这些发布序列分解,使它们成为团队发布序列。允许不同的团队安排自己的发布序列。最终,我们应该抛弃这些序列。它们应该只是迈向持续交付的一个步骤。遗憾的是,一些营销敏捷的优秀成果已经将发布序列作为交付软件的最终方式。我们知道他们已经这么做了,因为许多公司组织里挂着的 SAFe 图解上都印着“发布序列”的字样。这不是好事。不管是对于 SAFe,还是你遇到的任何其他问题,发布序列始终都是一种补救技术,是自行车的辅助轮。我们应该向着持续交付继续前进。问题是,如果我们使用这些发布序列时间太长,最终的架构就会是一个分布式单体,因为我们已经习惯了将所有服务部署在一起。要注意这一点。这可能不会在一夜之间发生。我们可以从支持独立部署的架构开始,但如果我们使用发布序列太久,我们的架构就会开始围绕这些发布实践聚合在一起。归根结底,分布式单体是一个问题,因为它同时具有分布式系统的所有复杂性和单个部署单元的缺点。我们应该跨越它,寻找更好的工作方式。分布式单体是一件很棘手的事情,关于如何处理这种情况的建议已经有很多。有时,正确的答案是将其合并回单进程单体。但是,如果现如今我们有一个分布式单体,最好的办法是弄清楚为什么我们会有这样的单体,并且在添加任何新服务之前,着手让架构中的某些部分可以独立部署。在这种情况下,添加新服务很可能会增加我们开展工作的难度。如何将单体迁移到微服务架构我们使用微服务架构是因为它具有独立部署的特性。我们希望能够在不改变其他任何东西的情况下将服务的更改部署到产品中。这是微服务的黄金法则。在演讲或文章中,这似乎很容易。在现实生活中,要做到这一点要困难得多,尤其是考虑到大多数人并非从零开始。绝大多数人都觉得他们的系统太大了,想把它分成更小的部分。他们想知道从哪里开始。领域驱动设计(DDD)有一些很好的方法可以帮助我们找出服务边界。当与研究微服务迁移的组织合作时,我们通常是从在现有的单体应用程序架构上执行 DDD 建模练习开始。我们这样做是为了弄清楚单体应用内部发生了什么,并从业务域的角度确定工作单元。尽管单体看起来像一个巨大的盒子,但当我们应用 DDD,并将逻辑模型投射到该单体上时,我们意识到,其内部被组织成订单管理、PDF 渲染、客户端通知等内容。虽然代码可能没有围绕这些概念进行组织,但从用户或业务领域模型的角度来看,这些概念存在于代码中。这些业务领域的边界(DDD 中通常称为“有界上下文”)就成为我们分解的单元,原因我这里就不展开讨论了。图 5:找出单体中的分解和依赖项单元首先要做的是问下从哪里开始,什么事情可以优先处理,我们的工作单元是什么。在图 5 所示的初始单体中,我们有订单管理、发票和通知。DDD 建模练习将使我们了解它们之间的关系。但愿我们能得出一个有向无环图,来描述这些不同功能之间的依赖关系。(如果我们得到是一个依赖关系的循环图,我们就得做更多的工作。)我们可以看到,在这个单体中,有很多东西都依赖于向客户发送通知的能力。那似乎是领域的核心部分。检查点:对于我遇到的问题,微服务是合适的解决方案吗?我们可以开始问问题了,比如我们应该先提取什么。我可以完全从这个角度来看问题。我们可能会看到,通知被很多功能使用——如果微服务更好,那么提取系统中很多部分都在使用的东西将使更多的东西变得更好。也许我们应该从那里开始。但是,看看所有的入站依赖关系。因为有太多的部分需要通知功能,所以我们很难将其从现有的单体架构中剥离出来。在这个单体系统中,像结账或订单管理之类的概念似乎更加独立。它们可能是更容易分解的东西。决定从哪一部分开始,从根本上讲是一种渐进式分解方法。首先要记住,单体并不是敌人。我希望大家都好好地思考一下。人们将任何单体系统都视为问题。在过去几年里,我看到的最令人担忧的事情之一是,微服务现在似乎成了许多人的默认选择。有人可能记得一句老话:“没有人会因为购买 IBM 产品而被解雇。”意思是,因为其他人都在买 IBM 产品,你也可以买——如果你买的东西不适合你,那也不是你的错,因为大家都在这么做。你没必要冒险。现在每个人都在做微服务,我们也面临同样的问题。每个人都吵着要做微服务。这对我很有好处:我写关于该主题的书,但对你可能不是好事。从根本上说,这取决于我们想要解决什么问题。我们想要达到而在当前的架构下无法达到的目标是什么?也许微服务是答案,或者其他什么东西才是答案。理解我们想要达到的目标至关重要,否则,我们将很难确定如何迁移我们的系统。我们正在做的事情将改变我们分解系统的方式,以及我们如何确定工作的优先级。微服务迁移不像一个开关,没有开/关切换。这更像是转动一个旋钮。在采用微服务的过程中,我们转动一下旋钮,增加一两个服务。我们想看看一个服务如何发挥作用,它是否提供了我们需要的东西,是否解决了我们的问题。如果是,而且我们也满意,我们就可以继续转动旋钮。不过,我看到很多人都会转动旋钮,增加 500 项服务,然后插上耳机,检查音量。这是让鼓膜破裂的好方法。我们不知道我们将要面对什么问题,那些问题在开发人员的笔记本电脑上碰不到。它们会在生产环境中出现。当我们从提供一个单体系统转为一次性提供 500 个服务时,所有的问题都会同时出现。不管我们最后是提供一项、两项还是五项服务,还是像 Monzo 那样拥有 800 项或 1500 项服务,我们都必须从一个小转变开始。我们需要选择一些服务来启动迁移。让它们在生产环境中运行,积累经验,并尽快把这种经验付诸实践。通过逐步调整,以渐进的方式创建和发布新的微服务,我们可以更好地发现和处理出现的问题。每个项目将要面对的问题都会有所不同,这取决于许多不同的因素。我们想要从单体系统中提取一些功能,让它与单体系统的剩余部分通信并集成,并且要尽快完成。我们不想再进行大爆炸式的重写了。我们过去是每年向用户发布软件,因为有一个为期 12 个月的窗口期,所以我们可以这样说:“现有的系统太糟糕了,现在已经无法使用了,但是我们还有 12 个月的时间来发布下一个版本。如果我们努力,完全可以重写系统,我们不会再犯过去犯过的错误,现有的功能一个都不会少,而且还会有更多的新功能,一切都会很好。”当每年发布一次软件时,我们从来没有那样做。当人们期望每月、每周或每天发布软件时,我不知道该如何证明其合理性。套用 Martin Fowler 的话来说,“ 如果你要进行大爆炸式的重写,你唯一能确定的就是大爆炸。 ”我喜欢动作片中的爆炸场面,但不喜欢我的 IT 项目里出现这种情况。我们需要从不同的角度思考如何做出这些更改。部署来自单体的第一个微服务我是架构增量演进的忠实拥护者。我们不应该认为我们的架构是一成不变的。我们需要有一些模式来帮助我们以渐进的方式向微服务转变。我们首先看下应用程序模式 Strangler Fig,它以一种植物命名,这种植物在树冠上生根,然后卷须向下缠绕在树干上。绞杀榕(strangler fig)靠自身无法爬到林冠层以获得足够的阳光,所以它不像普通树木一样从一棵小树苗慢慢长大,而是包裹在现有的植物体上。它依赖于现有的树的高度和力量。随着时间的推移,这些绞杀榕成长起来,变得越来越大,能够独立生存了。如果下面的树死了,腐烂了,就只剩下绞杀榕和一根空心的柱子。这些东西看起来就像蜡滴在其他树上——看起来真的很令人不安。但是作为应用程序迁移策略的一种模式,这种思想是有用的。我们找一个现有的系统(它完成我们想要它做的所有事情,即现有的单体应用程序),然后开始围绕它封装出我们的新系统。在这里,就是我们的微服务架构。实现 Strangler Fig 应用程序有两个关键。第一个是资产捕获,即确定把哪些功能迁移到微服务架构的过程。然后我们需要进行转接。以前对于单体应用程序的调用得转接到新功能上。如果功能没有迁移,调用就不需要转接;非常简单。有些人对如何转移功能感到困惑。如果我们真的够幸运的话,也许可以简单地复制代码。如果结账服务的代码在单体代码库中一个叫“结账”的漂亮盒子中,我们就可以剪切并粘贴到新服务中。我认为,如果代码库是这种状态,那你可能不需要任何帮助。更大的可能是,我们将不得不快速浏览系统,设法收集所有与结账相关的代码。我们可能会做一些重构前的活动。也许我们可以重用这些代码,但在这种情况下,那将是复制粘贴,而不是剪切粘贴。我们想把这个功能留在这个单体应用中,原因我将在后面讨论。更常见的情况是,人们会进行一些重写。实现 Strangler Fig 的方法有很多种。让我们来看一种简单的方法。假设我们有一个基于 HTTP 的单体系统。这可能是一个无头应用程序。我们可以在用户界面的后台使用 API Boundary 拦截调用。我们需要的是可以将调用重定向的东西,因此,我们将使用某种 HTTP 代理。对于这类架构,HTTP 协议非常有效,这是因为它非常适合透明的重定向调用。通过 HTTP 发起的调用可以被转接到许多不同的地方。有很多软件可以帮你做到这一点,而且非常简单。图 6:HTTP 代理拦截对单体的调用,增加了一个网络跃点首先要做的是,在上游流量和下游单体系统之间放置一个代理,别的什么都不用做。我们将把这个代理部署到生产环境中。此时,它还没有转接任何调用。我们可以看下它在生产环境中是否有效。我们要担心的一件事是网络质量,因为我们增加了一个网络跃点。通常是直接调用单体系统,但现在通过我们的代理。在这种情况下,延迟是杀手。通过代理转接只会给现有的调用增加几毫秒的开销——少于 10 毫秒就很棒。如果额外增加一个网络跃点增加了 200 毫秒的延迟,我们就需要暂停微服务迁移,因为我们还有其他需要首先解决的大问题。准备好代理之后,我们接下来将处理新的结账服务。我们将其部署到生产环境中。即使它功能还不全,也没什么问题,因为它还没有被使用。我们要在脑海中将部署到生产环境和使用这两个概念分开。开始采用微服务后,我们希望定期地将功能部署到生产环境中,以确保我们的部署机制能够正常工作。在添加功能时,我们可以单独测试新服务。我们还没有把它发布给用户,但它已经在生产环境中了。我们可以将它连接到我们的仪表板上,确保日志聚合正常,或者做其他我们想做的事。关键是我们只对一个服务进行操作。我们甚至可以将那个服务的提取过程分解为许多小步骤:创建服务框架、实现方法、在生产环境中测试它,然后部署发布版本。准备就绪之后,当我们认为新实现已经等同于旧系统时,我们只需重新配置代理,将调用从旧的单体功能转到新的微服务。事到如今,你可能认为现在应该从这个单体中删除旧功能。先别这么做!如果新创建的微服务在生产环境中出现问题,我们有一种非常快的补救技术:我们只需还原代理配置,将流量转到原功能所在的单体上。不过,要想实现这一点,我们必须考虑数据的作用——这点我们稍后讨论。我们希望,将这个功能提取成微服务是一种真正的重构,改变代码的结构而不是行为。在功能上,微服务应该等同于单体中的同一功能。我们应该能够在它们之间切换,直到微服务正常工作为止。如果我们想要保留切换的能力,那么在迁移完成、我们不再需要这种切换能力之前,我们就不应该添加新的功能或更改现有的功能。在很多情况下,这项简单的 Strangler Fig 技术都出奇的好。这个例子使用了 HTTP,但是我也看过使用 FTP 的情况。我已经用消息拦截器做到了这一点。我在上传固定文件时就这么做了:我们插入固定文件,在新服务中剔除我们希望去掉的内容,然后把剩下的内容传递下去。图 7:微服务架构的有向无环图使用“抽象分支“模式来逐步完成单体迁移Strangler Fig 对于结账或订单管理等功能非常有效,这些功能在我们的调用堆栈中处于更高的位置,如图 7 所示的依赖关系图。但是,进入单体系统的调用没有哪个是为了获得像忠诚奖励积分或给客户发送通知这样的能力。进入单体的调用是“下订单”或“付款”。只是作为这些操作的副作用,我们可能会奖励积分或发送电子邮件。因此,我们无法在单体系统的外围拦截对忠诚奖励积分或通知的调用。那是在单体系统内部完成的。假设我们要把通知功能提取出来。我们必须提取这块功能,并用一种增量的方式拦截这些入站链接,这样我们就不会破坏系统的其余部分。一种名为“抽象分支”的技术可以很好地完成这项工作。在基于主干的开发环境中,抽象分支是一种经常讨论的模式,这是一种很好的软件开发方式。在这种情况下中,抽象分支作为一种模式也很有用。我们在现有的单体系统中创建了一个空间,同一功能的两种实现可以在其中共存。在很多方面,这是 里氏替换原则 的一个例子。这是对完全相同的抽象做的一个独立实现。对于本例,我们将从现有代码中提取通知功能。图 8:用于迁移到一个微服务的抽象分支通知代码散布在我们的系统中。我们要做的第一件事是为新服务收集所有这些代码。我们将把服务隐藏在抽象点后面。我们希望结账代码和订单代码通过一个明确的抽象点来访问这个功能。起初,我们有一个通知抽象的实现——它封装了单体中当前所有与通知相关的功能。我们的所有调用——到 SMTP 库、到 Twilio、发送 SMS——都被打包到这个实现中。此时,我们所做的只是在代码中创建了一个很好的抽象点。我们可以停了。我们已经厘清了我们的代码库,并使其更容易测试,这已经是改进了。这是一种很好的老式重构。我们也创造了一个机会来更改结账或订单使用的通知功能的实现。我们可以用几天或几周的时间来完成这项重构工作,同时做一些其他的事情,比如实际发布特性。接下来,我们开始创建通知服务的新实现。这可以分成两个部分。我们已经在单体中实现了新接口,但这只是调用另一部分(新建的通知微服务)的客户端代码。部署这些实现很安全,因为它们还没有被使用。我们更频繁地集成代码,减少合并工作,并确保一切工作正常。一旦单体内部调用新服务的代码和单体外部的通知服务可以正常工作,我们所需要做的就是切换我们正在使用的抽象实现。我们可以使用特性开关、文本文件、专用工具,或者任何我们希望使用的方式。我们还没有删除旧功能,所以如果有问题,我们可以轻松切回旧功能。同样,这个服务的迁移被分解成许多小步骤,我们试图通过所有这些步骤尽快将其部署到生产环境。一切工作正常之后,我们就可以选择清理代码了。如果不再需要这个功能,我们可以删除其特性标识,甚或删除旧代码。现在删除旧代码很容易了,因为我们已经花了一些时间将所有代码整理好。我们删除了那个类,它消失了。我们把单体变小了,每个人都对自己感到满意。并行运行验证微服务迁移就代码重构而言,我强烈推荐 Michael Feathers 的著作《 修改代码的艺术 》。他对遗留代码的定义是没有测试代码的代码。关于如何在不破坏现有系统的情况下,在代码库中找出并创建这些抽象,这本书提供了很多好主意。即使你不使用微服务,仅仅创建这个抽象点就可能会使你的代码处于更好、更可测试的状态。我已经强调过,不要太早删除旧实现。保留两种实现有很多好处。它为我们如何部署和上线软件提供了有趣的方法。当调用进入抽象点时,它可以触发对这两个实现的调用。这叫做并行运行。这可以帮助我们确保新的微服务实现功能上等价。我们运行该功能的两个副本,然后比较结果。要做这个比较,只需运行这两个实现并比较结果。我们必须指定其中之一作为真相来源,因为我们不希望把两者串联起来:例如,在发送通知时,我们只想发送一封邮件,结果却发了两封。并行运行是一种实用而直接的实时比较,不仅是功能等价性的比较,而且包含可接受的非功能等价性比较。我们不仅要测试是否创建了正确的电子邮件,并将其发送到正确的虚拟 SMTP 服务器,而且还要测试新服务的响应速度是否同样快,或者错误率在可接受范围之内。通常,我们信任旧的功能实现,并使用其结果。我们将它们并行运行一段时间,如果新实现提供了可接受的结果,我们最终将处理掉旧的。GitHub 可以帮我们做这件事。他们创建了一个名为 GitHub Scientist 地库,这是一个很小的 Ruby 库,用于封装不同的抽象并对它们进行评分。在重构应用程序中的关键代码路径时,我们可以使用它来进行实时比较。GitHub Scientist 已经被移植到了很多不同的语言上,令人费解的是,Perl 有三种不同的移植:显然,在 Perl 社区,并行运行是一件很重要的事情。关于如何在应用程序内部并行运行,已经有很多很好的建议。将部署与发布分离:根本性的改变从根本上说,我们需要将部署的概念与发布的概念分离开来。传统上,我们认为这两种活动是一回事,部署软件和向生产环境用户发布软件是一回事。这就是为什么每个人都害怕生产环境会发生什么事情,这就是生产环境成为一个封闭环境的原因。我们可以把这两个概念分开。将某样东西部署到生产环境中与将它发布给我们的用户是不一样的。这个想法是人们现在所说的“渐进式交付”的基础,这是一个涵盖了一系列不同技术的总称,包括金丝雀发布、蓝/绿部署、抹黑启动等。我们可以快速推出软件,但不必向任何客户公开。我们可以把它放到生产环境中,在那里测试,然后自己承受出现的任何问题。如果我们将部署与发布分开,那么部署的风险就会小很多。我们就会更加勇敢地进行更改。我们将能够更频繁地发布,而且发布的风险将更低。RedMonk 联合创始人 James Governor 在公司的博客上对渐进式交付做了很好的 阐述 。该文探讨了渐进式交付,其中最重要的结论是,主动部署与主动发布不是一回事,并且你可以控制发布活动如何发生。用微服务方法迁移简单的数据访问我们将现有的单体应用程序和数据锁定在系统中,如图 9 所示。我们已经决定提取结账功能,但是它需要访问数据。图 9:从新服务访问旧数据选项一是直接访问单体的数据。如果我们仍然在测试并在单体中的结账功能和微服务里的结账功能之间进行切换,我们会希望这两种实现之间具有数据兼容性和一致性,这种方式可以保证这一点。这在短时间内是可以接受的,但它违背了数据库的黄金规则之一:不共享数据库。这不是可以长期依赖的东西,因为它会导致根本性的耦合问题。我们希望保持独立部署的能力。图 10:从新服务直接访问旧数据如图 10 所示,我们有一个 Shipping 服务和数据库,我们允许其他人访问我们的数据。我们已经向外部公开了内部实现细节。这使得 Shipping 服务的开发人员很难知道哪些内容可以安全地更改。哪些数据要共享,哪些数据要隐藏,并没有做区分。20 世纪 70 年代,David Parnas 提出了“信息隐藏”的概念,我们就是以此为基础考虑模块分解。我们希望在模块或微服务的边界内隐藏尽可能多的信息。如果我们创建一个定义良好的服务接口来共享数据,而不是直接公开数据库,那么这个接口就让 Shipping 服务的开发人员可以明确知道这个契约以及他们可以向外界公开什么。只要遵守该契约,开发人员就可以在 Shipping 服务中做任何他们想做的事情。也就是说,这些服务可以独立演进和开发。不要直接访问数据库,除非是在极其特殊的情况下。抛开直接访问,我们有两种选择:要么访问别人的数据,要么保存自己的数据。对于这个例子,假如我们已经确定新开发的结账微服务已经足够好,可以作为我们的真相来源。此时,如果我们想要使用别人的数据,那么这可能意味着数据属于单体,我们必须向单体请求数据。我们在单体上创建某种显式的服务接口(在我们的示例中是一个 API),通过它获取我们想要的数据。图 11:新开发的微服务使用单体显式提供的一个服务接口我们是结账服务,而不是订单服务,但我们可能需要订单数据。订单功能存在于单体中,因此我们将从那里获取数据。这样来说,我们需要在单体上定义服务接口以公开不同的数据集,而且,在这样做时,我们可以看到其他实体从单体中显现出来。我们可能会发现,订单服务正等待着从单体中喷发出来,就像《异形》中的异形幼体,在电影中,单体是由 John Hurt 扮演的,它会死去。另一种选择是保存服务自己的数据——在本例中,是单体数据库中的结账数据。至此,我们必须把数据移到一个结账数据库,这真的很难。从现有系统(尤其是关系数据库)中提取数据会带来很多麻烦。我们将看一个简单的例子,看看它带来的挑战。我将带大家深入了解一下,如何处理连接。最后的挑战:处理连接操作图 12:在线销售光盘的单体图 12 描述了一个现有的、在线销售光盘的单体应用程序。(你可以看出我这个例子已经用了多久了。)Catalog 功能知道某些东西多少钱,并将信息存储在 Line Items 表中。Finance 功能管理我们的财务交易,并将数据存储在 Ledger 表中。我们要做的其中一件事是,生成一个每周销量前 10 的专辑列表。在这种情况下,我们需要做一个简单的连接操作。我们从 Ledger 表上查出 10 个最畅销的。我们根据行和其他东西来限制这个查询。这样我们就能得到 ID 列表了。图 13:在线销售光盘的微服务架构在进入微服务领域后,我们需要在应用层执行连接操作。我们从 Finance 数据库中提取财务交易数据。关于我们出售的物品的信息则存在于 Catalog 数据库中。为了生成销量前 10 名列表,我们必须从 Ledger 表中提取最畅销商品的 ID,然后转到 Catalog 微服务,查询所销售商品的信息。我们过去在关系层中执行的连接操作转移到了应用程序层。延迟可能会变得令人震惊。现在,我不是做一个一次往返的连接操作,而是要调用 Finance 服务获取销量前 10 的 ID,然后调用另一个 Catalog 服务请求者 10 个 ID 的信息,然后 Catalog 服务从 Catalog 数据库中取得这些 ID,然后我们才得到响应。图 13 说明了这个过程。图 14:微服务架构会导致更多的跃点和延迟我们还没有涉及到像缺乏数据完整性这样的问题(在这种情况下,关系型数据库如何实现引用完整性)。小结如果你想深入研究诸如处理延迟和数据一致性之类的问题,我在《从单体到微服务》一书中进行了深入的阐述。无论你是否决定继续自己的微服务迁移之旅,我都建议你仔细考虑下,自己正在做什么以及为什么要这样做。不要把注意力都放在创建微服务上。相反,你要清楚自己想要达到的结果。你认为微服务会带来什么结果?专注于这一点——你可能会发现,你可以在不进入复杂的微服务世界的情况下实现同样的结果。以上就是有关微服务的学习笔记,希望可以对大家学习微服务有帮助,喜欢的小伙伴可以帮忙转发+关注,感谢大家!
netty系列之:使用Jboss Marshalling来序列化java对象
简介在JAVA程序中经常会用到序列化的场景,除了JDK自身提供的Serializable之外,还有一些第三方的产品可以实现对JAVA对象的序列化。其中比较有名的就是Google protobuf。当然,也有其他的比较出名的序列化工具,比如Kryo和JBoss Marshalling。今天想给大家介绍的就是JBoss Marshalling,为什么要介绍JBoss Marshalling呢?用过google protobuf的朋友可能都知道,虽然protobuf好用,但是需要先定义序列化对象的结构才能生成对应的protobuf文件。如果怕麻烦的朋友可能就不想考虑了。JBoss Marshalling就是在JDK自带的java.io.Serializable中进行优化的一个序列化工具,用起来非常的简单,并且和java.io.Serializable兼容,所以是居家必备开发程序的好帮手。根据JBoss官方的介绍,JBoss Marshalling和JDK java.io.Serializable相比有两个非常大的优点,第一个优点就是JBoss Marshalling解决了java.io.Serializable中使用的一些不便和问题。第二个优点就是JBoss Marshalling完全是可插拔的,这样就提供了对JBoss Marshalling框架进行扩展的可能,那么一起来看看JBoss Marshalling的使用吧。添加JBoss Marshalling依赖如果想用JBoss Marshalling,那么第一步就是添加JBoss Marshalling的依赖。很奇怪的是如果你到JBoss Marshalling的官网上,可能会发现JBoss Marshalling很久都没有更新了,它的最新版本还是2011-04-27年出的1.3.0.CR9版本。但是不要急,如果你去maven仓库搜一下,会发现最新的版本是2021年5月发行的2.0.12.Final版本。所以这里我们就拿最新的2.0.12.Final版本为例进行讲解。如果仔细观察JBoss Marshalling的maven仓库,可以看到JBoss Marshalling包含了4个依赖包,分别是JBoss Marshalling API,JBoss Marshalling River Protocol,JBoss Marshalling Serial Protocol和JBoss Marshalling OSGi Bundle。JBoss Marshalling API是我们在程序中需要调用的API接口,这个是必须要包含的。JBoss Marshalling River Protocol和JBoss Marshalling Serial Protocol是marshalling的两种实现方式,可以根据需要自行取舍。JBoss官网并没有太多关于这两个序列化实现的细节,我只能说,根据我的了解river的压缩程度更高。其他更多细节和实现可能只有具体阅读源码才知道了。JBoss Marshalling OSGi Bundle是一个基于OSGi的可插拔的框架。如果我们只是做对象的序列化,那么只需要使用JBoss Marshalling API和JBoss Marshalling River Protocol就行了。<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling</artifactId>
<version>2.0.12.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-river</artifactId>
<version>2.0.12.Final</version>
</dependency>JBoss Marshalling的使用添加了依赖之后,我们就可以开始使用JBoss Marshalling了。JBoss Marshalling的使用非常简单,首先我们要根据选择的marshalling方式创建MarshallerFactory:// 使用river作为marshalling的方式
MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river");这里我们选择使用river作为marshalling的序列化方式。有了MarshallerFactory,我们还需要一个MarshallingConfiguration为MarshallerFactory提供一些必要的序列化参数。一般来说,我们可以控制MarshallingConfiguration的下面一些属性:MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(4);
configuration.setClassCount(10);
configuration.setBufferSize(8096);
configuration.setInstanceCount(100);
configuration.setExceptionListener(new MarshallingException());
configuration.setClassResolver(new SimpleClassResolver(getClass().getClassLoader()));setVersion是设置使用的marshalling protocol的版本号,这个版本号非常重要,因为依赖的protocol实现可能根据会根据需要进行序列化实现的升级,可能产生不兼容的情况。通过设置版本号,可以保证升级之后的protocol也能兼容之前的序列化版本。setClassCount是预设要序列化对象中的class个数。setInstanceCount是预设序列化对象中的class实例个数。setBufferSize设置读取数据的buff大小,通过调节这个属性可以调整序列化的性能。setExceptionListener添加序列化异常的时候的异常监听器。setClassResolver用来设置classloader。JBoss Marshalling的强大之处在于我们在序列化的过程中还可以对对象进行拦截,从而进行日志输出或者其他的业务操作。configuration提供了两个方法,分别是setObjectPreResolver和setObjectResolver。这两个方法接受一个ObjectResolver对象,可以用来对对象进行处理。两个方法的不同在于执行的顺序,preResolver在所有的resolver之前执行。做好上面的配置之后,我们就可以正式开始编码了。final Marshaller marshaller = marshallerFactory.createMarshaller(configuration);
try(FileOutputStream os = new FileOutputStream(fileName)){
marshaller.start(Marshalling.createByteOutput(os));
marshaller.writeObject(obj);
marshaller.finish();
}上面的例子中,通过调用marshaller的start方法开启序列化,然后调用marshaller.writeObject写入对象。最后调用marshaller.finish结束序列化。整个序列化的代码如下所示:public void marshallingWrite(String fileName, Object obj) throws IOException {
// 使用river作为marshalling的方式
MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river");
// 创建marshalling的配置
MarshallingConfiguration configuration = new MarshallingConfiguration();
// 使用版本号4
configuration.setVersion(4);
final Marshaller marshaller = marshallerFactory.createMarshaller(configuration);
try(FileOutputStream os = new FileOutputStream(fileName)){
marshaller.start(Marshalling.createByteOutput(os));
marshaller.writeObject(obj);
marshaller.finish();
}
}
public static void main(String[] args) throws IOException {
MarshallingWriter writer = new MarshallingWriter();
Student student= new Student("jack", 18, "first grade");
writer.marshallingWrite("/tmp/marshall.txt",student);
}非常的简洁明了。注意,这里我们序列化了一个Student对象,这个对象一定要实现java.io.Serializable接口,否则会抛出类型下面的异常:Exception in thread "main" java.io.NotSerializableException:
at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:274)
at org.jboss.marshalling.AbstractObjectOutput.writeObject(AbstractObjectOutput.java:58)
at org.jboss.marshalling.AbstractMarshaller.writeObject(AbstractMarshaller.java:111)接下来就是序列化的反向动作反序列化了。代码很简单,我们直接上代码:public void marshallingRead(String fileName) throws IOException, ClassNotFoundException {
// 使用river协议创建MarshallerFactory
MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river");
// 创建配置文件
MarshallingConfiguration configuration = new MarshallingConfiguration();
// 使用版本号4
configuration.setVersion(4);
final Unmarshaller unmarshaller = marshallerFactory.createUnmarshaller(configuration);
try(FileInputStream is = new FileInputStream(fileName)){
unmarshaller.start(Marshalling.createByteInput(is));
Student student=(Student)unmarshaller.readObject();
log.info("student:{}",student);
unmarshaller.finish();
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
MarshallingReader reader= new MarshallingReader();
reader.marshallingRead("/tmp/marshall.txt");
}运行上面的代码,我们可能得到下面的输出:[main] INFO c.f.marshalling.MarshallingReader - student:Student(name=jack, age=18, className=first grade)可见读取序列化的对象已经成功。总结以上就是JBoss Marshalling的基本使用。通常对我们程序员来说,这个基本的使用已经足够了。除非你有根据复杂的序列化需求,比如对象中的密码需要在序列化的过程中进行替换,这种需求可以使用我们前面提到的ObjectResolver来实现。本文的例子可以参考:learn-netty4本文已收录于 http://www.flydean.com/17-jboss-marshalling/最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
设计模式之其他原则
如何理解 KISS 原则KISS 原则的英文描述有好几个版本,比如下面这几个。Keep It Simple and Stupid.Keep It Short and Simple.Keep It Simple and Straightforward.翻译过来意思就是“尽量保持简单”。如何写出满足 KISS 原则的代码?不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。如何理解 YAGNI 原则YAGNI 跟 KISS 说的是一回事吗?YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码。再比如,我们不要在项目中提前引入不需要依赖的开发包。对于 Java 程序员来说,我们经常使用 Maven 或者 Gradle 来管理依赖的类库(library)。我发现,有些同事为了避免开发中 library 包缺失而频繁地修改 Maven 或者 Gradle 配置文件,提前往项目里引入大量常用的 library 包。实际上,这样的做法也是违背 YAGNI 原则的。从刚刚的分析我们可以看出,YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。DRY 原则(Don’t Repeat Yourself)DRY 原则的定义非常简单,我就不再过度解读。今天,我们主要讲三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。这三种代码重复,有的看似违反 DRY,实际上并不违反;有的看似不违反,实际上却违反了。实现逻辑重复功能语义重复同一个项目中会有两个功能相同的函数。解决办法:统一实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。代码执行重复如何提高代码可复用性的一些方法有以下 7 点。减少代码耦合满足单一职责原则模块化业务与非业务逻辑分离通用代码下沉继承、多态、抽象、封装应用模板等设计模式Rule of Three 原则这条原则可以用在很多行业和场景中,你可以自己去研究一下。如果把这个原则用在这里,那就是说,我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后我们开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。也就是说,第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。迪米特法则 Demeter PrincipleLaw of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。什么是“高内聚、松耦合”?“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干。高内聚有助于松耦合,松耦合又需要高内聚的支持。如何利用迪米特法则来实现“高内聚、松耦合”?“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。有哪些代码设计是明显违背迪米特法则的?对此又该如何重构?不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。参考设计模式之美设计模式代码重构-极客时间https://time.geekbang.org/column/intro/250
JPA入门案例完成增删改查(1)
一、ORM思想ORM(Object Relational Mapping),对象关系映射,是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将java程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。将实体类与数据库表做队形,实体类中的属性与数据库中的字段做对应。这样就不用直接操作数据库,写SQL语句了,直接使用面向对象的技术,对象名.方法(),就可以实现对数据的增删改查等。二、JPA规范JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。JPA (Java Persistence API) Java持久化API。是一套Java官方制定的ORM 方案。JPA是一种规范,一种标准,具体的操作交给第三方框架去实现,比如说Hibernate,OpenJPA等。 Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。三、搭建JPA的基础环境1.创建数据库表 CREATE TABLE cst_customer (
cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客户编号(主键)',
cust_name varchar(32) NOT NULL COMMENT '客户名称(公司名称)',
cust_source varchar(32) DEFAULT NULL COMMENT '客户信息来源',
cust_industry varchar(32) DEFAULT NULL COMMENT '客户所属行业',
cust_level varchar(32) DEFAULT NULL COMMENT '客户级别',
cust_address varchar(128) DEFAULT NULL COMMENT '客户联系地址',
cust_phone varchar(64) DEFAULT NULL COMMENT '客户联系电话',
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;2.创建Maven工程导入坐标 <properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.hibernate.version>5.0.7.Final</project.hibernate.version>
</properties>
<dependencies>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- hibernate对jpa的支持包 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${project.hibernate.version}</version>
</dependency>
<!-- c3p0 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>${project.hibernate.version}</version>
</dependency>
<!-- log日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- Mysql and MariaDB -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
</dependencies>3.创建JPA的核心配置文件位置:配置到类路径下的一个叫做 META-INF 的文件夹下命名:persistence.xml<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<!--需要配置persistence-unit节点
持久化单元:
name:持久化单元名称
transaction-type:事务管理方式
JTA: 分布式事务管理
RESOURCE_LOCAL: 本地事务管理
-->
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
<!--jpa的实现方式-->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!--数据库信息-->
<!--可选配置:配置jpa实现方的配置信息-->
<properties>
<!--数据库信息-->
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="root"/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa"/>
<!--可选配置:配置jpa实现方的配置信息
显示SQL :false,true
自动创建数据库表:hibernate.hbm2ddl.auto
create:程序运行时创建数据库表,如果有表,先删除表再创建表
update:程序运行时创建数据库表,如果有表,不会创建表
none:不会创建表
-->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>4.编写实体类,配置映射关系实体与表的映射和实体类属性与表字段的映射 配置映射关系 1.实体类和表的映射关系 @Entity 声明是实体类 @Table(name = "cst_customer") 实体类与表的映射关系,name配置表的名称 2.实体类中属性和表字段的映射关系 @Column(name = "cust_id") @GeneratedValue:配置主键的生成策略 strategy GenerationType.IDENTITY :自增,mysql * 底层数据库必须支持自动增长(底层数据库支持的自动增长方式,对id自增) GenerationType.SEQUENCE : 序列,oracle * 底层数据库必须支持序列 GenerationType.TABLE : jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增* GenerationType.AUTO : 由程序自动的帮助我们选择主键生成策略/**
* @Author: Promsing(张有博)
* @Date: 2021/10/13 - 17:29
* @Description: 客户的实体类
* @version: 1.0
*/
@Entity
@Table(name = "cst_customer")
public class Customer {
/**
* @Id:声明主键的配置
* @GeneratedValue:配置主键的生成策略
* strategy
* GenerationType.IDENTITY :自增,mysql
* * 底层数据库必须支持自动增长(底层数据库支持的自动增长方式,对id自增)
* GenerationType.SEQUENCE : 序列,oracle
* * 底层数据库必须支持序列
* GenerationType.TABLE : jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增
* GenerationType.AUTO : 由程序自动的帮助我们选择主键生成策略
* @Column:配置属性和字段的映射关系
* name:数据库表中字段的名称
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Long id;//主键
@Column(name="cust_name")
private String custName;
@Column(name="cust_source")
private String custSource;
@Column(name = "cust_industry")
private String custIndustry;//所属行业
@Column(name="cust_level")
private String custLevel;
@Column(name = "cust_address")
private String custAddress;
@Column(name = "cost_phone")
private String custPhone;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCustName() {
return custName;
}
public void setCustName(String custName) {
this.custName = custName;
}
public String getCustSource() {
return custSource;
}
public void setCustSource(String custSource) {
this.custSource = custSource;
}
public String getCustIndustry() {
return custIndustry;
}
public void setCustIndustry(String custIndustry) {
this.custIndustry = custIndustry;
}
public String getCustLevel() {
return custLevel;
}
public void setCustLevel(String custLevel) {
this.custLevel = custLevel;
}
public String getCustAddress() {
return custAddress;
}
public void setCustAddress(String custAddress) {
this.custAddress = custAddress;
}
public String getCustPhone() {
return custPhone;
}
public void setCustPhone(String custPhone) {
this.custPhone = custPhone;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", custName='" + custName + '\'' +
", custSource='" + custSource + '\'' +
", custIndustry='" + custIndustry + '\'' +
", custLevel='" + custLevel + '\'' +
", custAddress='" + custAddress + '\'' +
", custPhone='" + custPhone + '\'' +
'}';
}
}
SonarQube使用介绍
SonarQube使用介绍SonarQube® 是一种自动代码审查工具,用于检测代码中的错误、漏洞和代码异味。它可以与您现有的工作流程集成,以实现跨项目分支和拉取请求的持续代码检查。SonarQube 是一个用于代码质量管理的开源平台,用于管理源代码的质量。 通过插件形式,可以支持包括 java, C#, C/C++, PL/SQL, Cobol, JavaScrip, Groovy 等等二十几种编程语言的代码质量管理与检测。工作流程: 工作流程介绍1.开发人员在IDE中开发和合并代码,并将代码签入到DevOps平台。2.持续集成工具(如jenkins)检查、构建和运行单元测试,集成SonarQube扫描仪分析结果。3.扫描程序将结果发布到SonarQube服务器,该服务器通过SonarQube界面、电子邮件、IDE内通知(通过SonarLint)和拉入或合并请求的装饰(使用Developer Edition及更高版本时)向开发人员提供反馈。主要作用编写整洁代码把出现在代码里的新问题都解决掉,就可以创建并维护一个干净的代码基础。即使是遗留项目,保持新代码的整洁,也能最终获得一个值得骄傲的代码基础。修复代码缺陷缺陷图例和默认质量阈都是基于新代码周期的 - 当前周期就是处理问题的时间。主要的关注点是上一个版本,通常会选择30天作为一个周期。加强质量阈项目的质量阈是在发布到生产环境之前所需要达到的一系列的条件标准。质量阈可以确保下一个版本的代码质量总能高于上一个版本。Sonar的优点:(1)支持所有语言的检测。一个工具,搞定所有。 (2)灵活扩展,插拔式使用。自定义的代码检测规则,可自定义插件,独立打成JAR包放到SONARQUBE插件目录下,重启即生效,开发使用非常方便。而且自带UT验证框架,开发效率高。(3)规则支持多租户隔离。租户可定制自己的规则集。(4)生态强大,业界有诸多插件,与jenkins友好集成。(5)部署使用便捷。(6)架构松耦合,通过与maven/jenkins等集成,将代码扫描的计算消耗迁移到业务或者构建方的资源上,极大的提升了自身的吞吐能力。衡量代码质量的几个指标1.Bugs Bug是出现了明显错误或是高度近似期望之外行为的代码。2.漏洞 漏洞是指代码中可能出现被黑客利用的潜在风险点。3.安全热点 安全敏感代码需要手工审核,以便判断是否存在安全漏洞。4.异味 代码异味会困扰代码的维护者并降低他们的开发效率。主要的衡量标准是修复它们所需的时间。5.重复率 新代码中的重复行密度 (%),重复行数,重复代码块6.行数 程序中代码的行数SonarQube的UI界面: 等等sonarqube的官网个人解决项目中的bug,异味总结①:变量声明后不使用,多余变量正例:localValue/getHttpMessage()/GroupController②:方法名、变量名不符合命名规范例如:方法名、参数名统一使用驼峰命名法(Camel命名法),除首字母外,其他单词的首字母大写,其他字母小写,类名每个组合的单词都要大写;③:常量命名不规范 禁止缩写。命名尽量简短,不要超过16个字符采用完整的英文大写单词,在词与词之间用下划线连接,如:DEFAULT_VALUE。 同一组的常量可以用常量类封装在一起,便于引用和维护代码中用到常量的,用静态常量表示,正例:Private final static String SUCCESS=”success”;Private final static String ERROR = “error”;调用:类名.SUCCESS ④:删除无用的依赖import中灰色的部分⑤:禁止使用 System.out.println(""); 打印内容推荐使用private static Log logger = LogFactory.getLog(AopGetService.class);或者log.info("章节拼接成功!");⑥:Controller类中不要抛出异常,需要用try,catch捕获⑦:删除无用的注释,例如用于测试的代码⑧:将程序中的 //TODO 尽快完成⑨:等等~~
解决IDEA编译乱码 Build Output提示信息乱码�����
问题说明:IDEA编译的时候乱码,Build Output提示信息乱码�����。解决方案一:将Help—>Edit Cusuom VM Options...中添加 -Dfile.encoding=UTF-8-Xms128m
-Xmx1524m
-XX:ReservedCodeCacheSize=512m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:CICompilerCount=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-ea
-Dsun.io.useCanonCaches=false
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
-Dkotlinx.coroutines.debug=off
-Dide.run.dashboard=true
-Dfile.encoding=UTF-8
-javaagent:C:\Users\Public\.BetterIntelliJ\BetterIntelliJ-1.16.jar解决方案二:防止方案一未生效,可用方案二在IDEA的安装目录下找到idea.exe.vmoptions和idea64.exe.vmoptions文件将 -Dfile.encoding=UTF-8 粘贴进去解决方案三:Settings->Editor->File Encodings 全部改为UTF-8 更改文件的编码格式解决方案四:Settings->Build,Ex***->Compiler-> 在Java Compiler中添加参数:-encoding utf-8解决方案五:选择适合本工程的JDK版本解决方案六:找到Maven的配置 取消委托给Maven的build/run的权利。上面解决方案有很多,总有一个适合你的。加油。
手把手教你实现一个方法耗时统计的 java agent
前面有两篇铺垫博文,在博文《200303-如何优雅的在 java 中统计代码块耗时》,其最后提到了根据利用 java agent 来统计方法耗时博文《200316-IDEA + maven 零基础构建 java agent 项目》中则详细描述了搭建一个 java agent 开发测试项目的全过程本篇博文将进入 java agent 的实战,手把手教你如何是实现一个统计方法耗时的 java agent1. 基本姿势点上面两节虽然手把手教你实现了一个 hello world 版 agent,然而实际上对 java agent 依然是一脸茫然,所以我们得先补齐一下基础知识首先来看 agent 的两个方法中的参数 Instrumentation,我们先看一下它的接口定义/**
* 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
* Transformer可以直接对类的字节码byte[]进行修改
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
复制代码前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。通过上面的描述,可知可以通过Transformer修改类类加载时,会被触发 Transformer 拦截2. 实现我们需要统计方法耗时,所以想到的就是在方法的执行前,记录一个时间,执行完之后统计一下时间差,即为耗时直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码实现自定义的ClassFileTransformer,代码如下public class CostTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 这里我们限制下,只针对目标包下进行耗时统计
if (!className.startsWith("com/git/hui/java/")) {
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
for (CtMethod method : cl.getDeclaredMethods()) {
// 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
method.addLocalVariable("start", CtClass.longType);
method.insertBefore("start = System.currentTimeMillis();");
String methodName = method.getLongName();
method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +
".currentTimeMillis() - start));");
}
byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
复制代码然后稍微改一下 agent/**
* Created by @author yihui in 16:39 20/3/15.
*/
public class SimpleAgent {
/**
* jvm 参数形式启动,运行此方法
*
* manifest需要配置属性Premain-Class
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
customLogic(inst);
}
/**
* 动态 attach 方式启动,运行此方法
*
* manifest需要配置属性Agent-Class
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
customLogic(inst);
}
/**
* 统计方法耗时
*
* @param inst
*/
private static void customLogic(Instrumentation inst) {
inst.addTransformer(new CostTransformer(), true);
}
}
复制代码到此 agent 完毕,打包和上面的过程一样,接下来进入测试环节创建一个 DemoClz, 里面两个方法public class DemoClz {
public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}
public int count(int i) {
System.out.println("cnt: " + i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 1;
}
}
复制代码然后对应的 main 方法如下public class BaseMain {
public static void main(String[] args) throws InterruptedException {
DemoClz demoClz = new DemoClz();
int cnt = 0;
for (int i = 0; i < 20; i++) {
if (++cnt % 2 == 0) {
i = demoClz.print(i);
} else {
i = demoClz.count(i);
}
}
}
}
复制代码选择 jvm 参数指定 agent 方式运行(具体操作和上面一样),输出如下虽然我们的应用程序中并没有方法的耗时统计,但是最终的输出却完美的打印了每个方法的调用耗时,实现了无侵入的耗时统计功能到这里本文的 java agent 的扫盲 + 实战(开发一个方法耗时统计)都已经完成了,是否就宣告着可以小结了,并不是,下面介绍一下在实现上面的 demo 过程中遇到的一个问题3. Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame在演示方法耗时的 agent 的示例中,并没有借助最开始的测试用例,而是新建了一个DemoClz来做的,那么为什么这样选择呢,如果直接用第二节的测试用例会怎样呢?public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}
public void run() {
int i = 1;
while (true) {
i = print(i);
}
}
public static void main(String[] args) {
BaseMain main = new BaseMain();
main.run();
}
复制代码依然通过 jvm 参数指定 agent 的方式,运行上面的代码,会发现抛异常,无法正常运行了指出了在 run 方法这里,存在字节码的错误,我们统计耗时的 Agent,主要就是在方法开始前和结束后各自新增了一行代码,我们直接补充在 run 方法中,则相当于下面的代码上面的提示很明显的告诉了,最后一行语句永远不可能达到,编译就存在异常了;那么问题来了,作为一个 java agent 的提供者,我哪知道使用者有没有写这种死循环的方法,如果应用中有这么个死循环的任务存在,把我的 agent 一挂载上去,导致应用都起不来,这个锅算谁的????下面提供解决方案,也很简单,在 jvm 参数中,添加一个-noverify (请注意不同的 jdk 版本,参数可能不一样,我的本地是 jdk8,用这个参数;如果是 jdk7 可以试一下-XX:-UseSplitVerifier)在 IDEA 开发环境下,如下配置即可再次运行,正常了4. 小结本篇为实战项目,首先明确方法参数Instrumentation它的接口定义,通过它来实现 java 字节码的修改我们通过实现自定义的ClassFileTransformer,借助 javassist 来修改字节码,为每个方法的第一行和最后一行注入耗时统计的代码,从而实现方法耗时统计最后留一个小问题,上面的实现中,当方法内部抛出异常时,我们注入的最后一行统计耗时会不会如期输出,如果不会,应该怎么修改,欢迎各位大佬留言指出解决方案
IDEA + maven 零基础构建 java agent 项目
Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的;虽说在实际的业务开发中,很少会涉及到 agent 开发,但是每个 java 开发都用过,比如使用 idea 写了个 HelloWorld.java,并运行一下, 仔细看控制台输出本篇将作为 Java Agent 的入门篇,手把手教你开发一个统计方法耗时的 Java AgentI. Java Agent 开发首先明确我们的开发环境,选择 IDEA 作为编辑器,maven 进行包管理1. 核心逻辑创建一个新的项目(or 子 module),然后我们新建一个 SimpleAgent 类public class SimpleAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
}
}
复制代码我们先忽略上面两个方法的具体玩法,先简单看一下这两个方法的区别,注释上也说了jvm 参数形式: 调用 premain 方法attach 方式: 调用 agentmain 方法其中 jvm 方式,也就是说要使用这个 agent 的目标应用,在启动的时候,需要指定 jvm 参数 -javaagent:xxx.jar,当我们提供的 agent 属于基础必备服务时,可以用这种方式当目标应用程序启动之后,并没有添加-javaagent加载我们的 agent,依然希望目标程序使用我们的 agent,这时候就可以使用 attach 方式来使用(后面会介绍具体的使用姿势),自然而然的会想到如果我们的 agent 用来 debug 定位问题,就可以用这种方式2. 打包上面一个简单 SimpleAgent 就把我们的 Agent 的核心功能写完了(就是这么简单),接下来需要打一个 Jar 包通过 maven 插件,可以比较简单的输出一个合规的 java agent 包,有两种常见的使用姿势a. pom 指定配置在 pom.xml 文件中,添加如下配置,请注意一下manifestEntries标签内的参数<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>
<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
复制代码然后通过 mvn assembly:assembly 命令打包,在target目录下,可以看到一个后缀为jar-with-dependencies的 jar 包,就是我们的目标b. MANIFEST.MF 配置文件通过配置文件MANIFEST.MF,可能更加常见,这里也简单介绍下使用姿势在资源目录(Resources)下,新建目录META-INF在META-INF目录下,新建文件MANIFEST.MF文件内容如下Manifest-Version: 1.0
Premain-Class: com.git.hui.agent.SimpleAgent
Agent-Class: com.git.hui.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
复制代码请注意,最后的一个空行(如果我上面没有显示的话,多半是 markdown 渲染有问题),不能少,在 idea 中,删除最后一行时,会有错误提醒然后我们的pom.xml配置,需要作出对应的修改<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<!--<manifestEntries>-->
<!--<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>-->
<!--<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>-->
<!--<Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--<Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--</manifestEntries>-->
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
复制代码同样通过mvn assembly:assembly命令打包II. Agent 使用agent 有了,接下来就是需要测试一下使用 agent 的使用了,上面提出了两种方式,我们下面分别进行说明1. jvm 参数首先新建一个 demo 项目,写一个简单的测试类public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
return i + 2;
}
public void run() {
int i = 1;
while (true) {
i = print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
BaseMain main = new BaseMain();
main.run();
Thread.sleep(1000 * 60 * 60);
}
}
复制代码测试类中,有一个死循环,各 1s 调用一下 print 方法,IDEA 测试时,可以直接在配置类,添加 jvm 参数,如下请注意上面红框的内容为上一节打包的 agent 绝对地址: -javaagent:/Users/..../target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar执行 main 方法之后,会看到控制台输出请注意上面的premain, 这个就是我们上面的SimpleAgent中的premain方法输出,且只输出了一次2. attach 方式在使用 attach 方式时,可以简单的理解为要将我们的 agent 注入到目标的应用程序中,所以我们需要自己起一个程序来完成这件事情public class AttachMain {
public static void main(String[] args)
throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
// attach方法参数为目标应用程序的进程号
VirtualMachine vm = VirtualMachine.attach("36633");
// 请用你自己的agent绝对地址,替换这个
vm.loadAgent("/Users/......./target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
复制代码上面的逻辑比较简单,首先通过jps -l获取目标应用的进程号当上面的 main 方法执行完毕之后,控制台会输出类似下面的两行日志,可以简单的理解为我连上目标应用,并丢了一个 agent,然后挥一挥衣袖不带走任何云彩的离开了Connected to the target VM, address: '127.0.0.1:63710', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:63710', transport: 'socket'
复制代码接下来再看一下上面的 BaseMain 的输出,中间夹着一行agentmain, 就表明 agent 被成功注入进去了3. 小结本文介绍了 maven + idea 环境下,手把手教你开发一个 hello world 版 JavaAgent 并打包的全过程两个方法方法说明使用姿势premain()agent 以 jvm 方式加载时调用,即目标应用在启动时,指定了 agent-javaagent:xxx.jaragentmain()agent 以 attach 方式运行时调用,目标应用程序正常工作时使用VirtualMachine.attach(pid)来指定目标进程号 vm.loadAgent("...jar")加载 agent两种打包姿势打包为可用的 java agent 时,需要注意配置参数,上面提供了两种方式,一个是直接在pom.xml中指定配置<manifestEntries>
<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>
<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
复制代码另外一个是在配置文件 META-INF/MANIFEST.MF 中写好(需要注意最后一个空行不可或缺)Manifest-Version: 1.0
Premain-Class: com.git.hui.agent.SimpleAgent
Agent-Class: com.git.hui.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
复制代码当然本篇内容看完之后,会发现对 java agent 的实际开发还是不太清楚,难道 agent 就是在前面输出一行hello world就完事了么,这和想象中的完全不一样啊下一篇博文将手把手教你实现一个方法统计耗时的 java agent 包,将详细说明利用接口Instrumentation来实现字节码修改,从而是实现功能增强II. 其他0. 源码github.com/liuyueyi/ja…