开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

修改Git提交历史中的author,email和name等信

发表于 2021-11-30

这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战

在我们日常工作中,通常由于某种原因,需要更新历史提交的commit信息。一般会有如下几种场景:

  • 修改当前分支最新 commit 的提交信息
  • 修改当前分支某历史commit的提交信息
  • 修改当前分支所有提交的commit信息

对于当次提交来说,我们可以显示指定提交者信息。

git commit -m "Initial commit" --author="mn <mn@furzoom.com>"

通过git commit命令将暂存区内容添加到本地仓库后,git会生成相应的commit id。后续我们就可以针对指定的commit id 进行操作,比如,回退到某个commit id,拉取指定commit id的代码等。

下面我们针对上面的三种情况进行讲述。

修改上次提交的commit信息

git commit --amend --author="果冻<liguodongiot@163.com>"

输入git commit --amend之后,进入编辑模式,修改提交信息,然后按wq保存退出。

如果不想修改提交信息,则添加--no-edit,如下所示:

git commit --amend --author="果冻不吃皮 <mn@furzoom.com>" --no-edit

git commit --amend命令只会修改最后一次commit的信息,之前的commit需要使用git rebase

修改上几次提交的commit信息

git rebase -i说明:

git rebase -i命令可以压缩合并多次提交。

格式:git rebase -i [startpoint] [endpoint]

其中,-i的意思是–interactive,即弹出交互式的界面让用户编辑完成合并操作,[startpoint] [endpoint]则指定了一个编辑区间,如果不指定 [endpoint] ,则该区间的终点默认是当前分支HEAD所指向的commit(该区间指定的是一个前开后闭的区间)。

首先,我们通过git rebase -i选择将哪些提交获得重定位。

例如,合并最近的三次提交:

git rebase -i HEAD~3

或者,合并从当前head到15f745b(commit id)的提交:

git rebase -i 15f745b

然后,在列表中将开头的pick修改为edit,然后重复执行以下命令直到完成:

git commit --amend --author="mn <mn@furzoom.com>"

之后,通过continue命令回到正常状态。

git rebase –continue

修改之前某一个特定的commit信息

查看log,找到上一次提交的commit id

git log

然后,通过git rebase到要修改的commit之前那个commit。

git rebase 928fc8a3686bf5fcf4527873e075703a9998c127 –interactive(与上面git rebase -i类似)

然后,在vi中修改pick为edit,wq保存退出,接着进行内容修改,git add后git commit --amend。

修改所有提交的commit信息

前提:重置本项目用户的信息

1
2
bash复制代码git config user.name '果冻'
git config user.email 'liguodongiot@163.com'

方案一:git rebase

首先回到当前分支第一个commit。

git rebase -i –root

然后,弹出编辑器,在需要修改的commit处,将picked改变为edit,然后wq,退出vi。

git commit –amend –reset-author

之后,通过continue命令回到正常状态。

git rebase –continue

查看日志,确定是否修改成功

git log

最后,强制push到远程仓库

git push origin master -f

方案二:git filter-branch

使用git rebase方式,步骤比较多,可以直接使用 git filter-branch快速方便。

例如,将提交者liguodongiot@163.com修改为liguodongiot@foxmail.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bash复制代码# !/bin/sh

git filter-branch --env-filter '

OLD_EMAIL="liguodongiot@163.com"
CORRECT_NAME="吃果冻不吐果冻皮"
CORRECT_EMAIL="liguodongiot@foxmail.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]

then

export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"

fi

if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]

then

export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"

fi

' --tag-name-filter cat -- --branches --tags

总结

修改 commit 提交信息场景 操作命令
修改最新 commit 的提交信息 git commit --amend
修改某历史 commit 的提交信息 git rebase -i father_commitId,其中father_commitId表示上一个commitId
修改第一个 commit 的提交信息 git rebase -i --root

注意:

如需将修改信息同步到远端仓库,可使用 git push -f 命令进行强制同步,该操作会覆盖远端分支的提交历史,请自行确认操作风险。

参考文档

  • 如何修改第一个 commit 的提交信息
  • 修改git提交历史中的author和email
  • 如何修改git commit的author信息
  • git修改提交历史中的author信息
  • git rebase -i合并多次提交

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Git 你也许遇到过Commit操作这些问题!

发表于 2021-11-30

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

git commit 是我们常用的命令,我们可以通过这个命令提交代码,是不是有时候可能出现提交信息写错了,想删除某一次提交记录,甚至想找回某一个reset命令后重置的内容。在这篇文章,上面所提到的内容都能解决。

一、提交信息写错了

如果你的提交信息(commit message)写错了且这次提交(commit)还没有推(push), 你可以通过下面的方法来修改提交信息(commit message):

1
css复制代码$ git commit --amend --only

这会打开你的默认编辑器, 在这里你可以编辑信息. 另一方面, 你也可以用一条命令一次完成:

1
css复制代码$ git commit --amend --only -m 'xxxxxxx'

如果你已经推(push)了这次提交(commit), 你可以修改这次提交(commit)然后强推(force push), 但是强烈不推荐这么做。

二、删除任意提交内容

有可能在某次提交过程中你上传了本应该存在于Local的文件内容,比如账号密码等,这个时候你想删除这次commit的记录。

同样的警告:不到万不得已的时候不要这么做.

1
2
css复制代码$ git rebase --onto SHA1_OF_BAD_COMMIT^ SHA1_OF_BAD_COMMIT
$ git push -f [remote] [branch]

或者做一个 交互式rebase 删除那些你想要删除的提交(commit)里所对应的行。

三、找回reset hard操作的内容

如果你意外的做了 git reset --hard, 你通常能找回你的提交(commit), 因为Git对每件事都会有日志,且都会保存几天。

1
css复制代码(main)$ git reflog

你将会看到一个你过去提交(commit)的列表, 和一个重置的提交。 选择你想要回到的提交(commit)的SHA,再重置一次:

1
css复制代码(main)$ git reset --hard SHA1234

这样就完成了。

四、从一次提交中移除一个文件

通过下面的方法,从一个提交(commit)里移除一个文件:

1
2
3
ruby复制代码$ git checkout HEAD^ myfile
$ git add -A
$ git commit --amend

这将非常有用,当你有一个开放的补丁(open patch),你往上面提交了一个不必要的文件,你需要强推(force push)去更新这个远程补丁。


少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用jersey的客户端API封装rest客户端优化建议 前

发表于 2021-11-30

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

前言

很早之前就在用jersey提供的客户端API封装rest客户端,jersey提供的客户端API,简单易用,可惜这个框架有点重,在使用上因为某些环境原因会引发一些不必要的性能问题,这里根据以往的经验列出几条优化的建议。

三步构造rest客户端

如果用jersey提供的API构造客户端,主要分3步,如下:

  1. 初始化Client实例(管理通信的基础设施)
  2. 构造WebTarget实例(资源定位)
  3. 构造Invocation实例(发起调用)

这里不具体说明了,如果需要代码示例,可以看官方文档,说的很清楚了,这是链接:jersey.github.io/documentati…

优化在于细节

这里主要说一下在开发过程中需要注意的几个点:

NO.1 Client实例的单例性

Client实例负责管理低层通信的基础设施,是一个重量级对象(源码注释说的),应该尽可能少的构造它的实例。

这里注意两点:


1. 不要每次请求构造一个Client实例


2. 尽可能少的构造它的实例。注意我说的尽可能少,下面解释下:


虽然标题我列的是单例,但并不一定全局就构造一个实例,可以视情况而定,考虑用懒初始化的方式构造几个不同的客户端实例缓存起来,比如:如果是普通的http请求了,就用一个基本配置,没有多余特性能力支持配置客户端调用就行;如果想让本次请求支持https协议,那就用一个支持https协议调用的客户端;如果是一些文件上传下载等请求,就用一个注册 了Multipart特性的客户端;请求想支持连接池,那就用支持连接池的客户端...等等。


但是,千万不要想着全局只用一个客户端注册了以上需要的所有特性,这并不是一个好主意。其实我最开始就犯过这样的错误,直到后来在性能上遇到瓶颈研究源码时才明白,在客户端配置上注册的某些特性支持,即使当前请求并不需要,也可能会有相关的性能或耗时操作,相反真正需要这些特性支持的请求,偶尔才会过来几条。


不要疑惑哪种请求用哪种客户端,这应该是开发的时候按照相关协议或约定来区分的。总之,尽量保证每个客户端实例的轻量特性。

NO.2 不要频繁构造无参的ClientConfig实例

只在构造Client实例的时候调用ClientConfig的无参构造方法,其它情况下就不要调用了。Client的单例性,也保证开发时调用它无参构造方法的次数限制。


虽然看起来只是new了一个ClientConfig的空构造方法,实例化了一个ClientConfig对象,但是在调用它的无参构造方法时,会初始化它的一个连接器提供者的属性,而在初始化这个属性的时候,会扫描应用类路径、扩展类路径、引导类路径下的所有jar包文件,期望通过它的SPI扩展方式查找到一个连接器提供者,其实很多时候我们不会去使用它的这些扩展,所以最终因为没找到还是会用默认的连接器。但是已经做了很多无用功,并且在扫找类路径遍历文件的时候,每次循环都会有一个同步处理的操作(调用的JDK的JarFile内的方法)。有同步就有锁,这样就不好了,并发高的时候,CPU调度慢,锁占有时间久,资源释放慢,就容易有大量线程因此阻塞。而这个锁间接的是我们自己加上的。


所以不要无谓的调用 ClientConfig的无参构造方法。

NO.3 使用连接池

使用连接池,见名知义,我觉得这个好处就不用我多说了,但是有一些需要注意的地方:

1. 默认的连接器是HttpUrlConnector,这个是不支持连接池的,使用连接池的话可以考虑更换其它连接器,比如:ApacheConnector。当然也可以自定义。


2. 读取超时/连接超时配置,连接池本身就是缓存了一些闲置可用的长连接,所以,对于这些连接还怎么设置它的一次请求中的连接超时或者读取超时,其它我也不知道。

NO.4 关闭类路径扫描

官方文档中有这么一段话:

Jersey uses a common Java Service Provider mechanism to obtain all service implementations. It means that Jersey scans the whole class path to find appropriate META-INF/services/ files. The class path scanning may be time consuming. The more jar or war files on the classpath the longer the scanning time. In use cases where you need to save every millisecond of application bootstrap time, you may typically want to disable the services provider lookup in Jersey.

请注意,这个类路径扫描是存在同步操作的,另外它的耗时是跟类路径下jar包或war包的数量相关。

所以jersey提供了关闭这个特性,但是又不影响核心功能的方式:

Since it is possible to configure all SPI implementation classes or instances manually in your Application subclass, disabling services lookup in Jersey does not affect any functionality of Jersey core modules and extensions and can save dozens of ms during application initialization in exchange for a more verbose application configuration code.

The services lookup in Jersey (enabled by default) can be disabled via a dedicated CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE property. There is a client/server counter-part that only disables the feature on the client or server respectively:ClientProperties.METAINF_SERVICES_LOOKUP_DISABLE/ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE. As in all other cases, the client/server specific properties overrides the value of the related common property, when set.

当然了,在配置中启用这个属性并不代表就不会扫描类路径了,还可能在其它地方,比如如果启用Multipart的特性支持,每次请求就会扫描类路径下是否有相关配置,这个关闭不了,所以前面也建议针对不同请求使用不同的客户端。另外,jersey代码内部有一些方法调用依然会触发类路径扫描,但是经过我仔细研究它的源码,找到了一种方法通过自定义扩展的方式可以避免,后续有需要会再说明。

其它的突然之前也想不起来了,最后一个建议,框架选型很重要,比如:如果只是功能要求不多,但是性能要高,可以考虑找一款更轻量级的rest客户端,就不要考虑用jersey了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

工作中常用到的Java8特性

发表于 2021-11-30

Lambda表达式

lambda表达式是是一个匿名函数,即没有函数名的函数

举个我们平时创建线程的栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码 //Runnable实现类
public class RunnableImpl implements Runnable {
     @Override
     public void run() {
         logger.info("Runnable实现类创建并启动一个新的线程");
    }
 }
 ​
 //采用传统的Runnable对象作为参数新建并启动一个线程
 new Thread(new RunnableImpl()).start();
 ​
 //采用匿名内部类的方式新建并启动一个线程
 new Thread(new Runnable() {
     @Override
     public void run() {
         logger.info("匿名内部类方式创建并启动一个新的线程");
    }
 }).start();
 ​
 //对匿名内部类进行简化,写成lambda表达式
 new Thread(()-> logger.info("lambda表达式创建并启动一个新的线程")).start();
 ​

优点:lambda表达式相比传统的Runnable对象作为参数传递给Thread类方式创建线程的方式,代码更简洁

方法引用

方法引用可以将一个方法封装成一个变量。::双冒号是方法引用的符号

1
2
3
java复制代码 //方法引用,将字符串100转换成Integer类型
 Function<String, Integer> function = Integer::parseInt;
 Integer IntegerResult= function.apply("100");

方法引用的返回值类型是函数式接口

被引用方法的参数个数,类型,返回值类型需要和函数式接口中方法的声明一致,只要满足这个要求,可以返回任意类型的函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码 //Function函数式接口,apply方法接收T类型的变量,返回R类型的结果
 @FunctionalInterface
 public interface Function<T, R> {
     R apply(T t);
 }
 ​
 Integer.paseInt()方法接收字符串类型参数,返回Integer类型的结果
   
 //比较两个int值大小,下面两种返回值类型均可
 Comparator<Integer> compare = Integer::compare;
 int compare1 = compare.compare(1, 2);
 ​
 IntBinaryOperator compare = Integer::compare;
 int compare1 = compare.applyAsInt(1, 2);

在java.util.function包下有很多函数式接口

image.png

自定义一个函数式接口

1
2
3
4
5
6
csharp复制代码 //函数式接口的run方法会接收T和S类型两个参数,返回R类型的结果
 @FunctionalInterface
 public interface KiteFunction<T,R,S> {
     
     R run(T t, S s);
 }

自定义一个和KiteFunction的run方法队对应的方法

1
2
3
4
5
6
7
8
9
typescript复制代码 //dateFormat方法的功能是按照指定的格式,把日期进行格式化,返回字符串的日期
 public class TimeConverse {
 ​
     public static String dateFormat(LocalDateTime localDateTime,String pattern){
         DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
         String dateStr = localDateTime.format(dateTimeFormatter);
         return dateStr;
    }
 }
1
2
3
ini复制代码 KiteFunction<LocalDateTime, String, String> dateFormat = TimeConverse::dateFormat;
 String dateStr = dateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
 System.out.println(dateStr);

如果TimeConverse的dateFormat方法只会被调用一次,我们也可以直接将其在kiteFunction接口的run方法中实现(原理类似于Runnable接口创建启动一个新线程),这种是匿名内部类的方式

1
2
3
4
5
6
7
8
typescript复制代码 String dateStr2 = new KiteFunction<LocalDateTime, String, String>() {
             @Override
             public String run(LocalDateTime localDateTime, String pattern) {
                 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
                 return localDateTime.format(dateTimeFormatter);
            }
 ​
 }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");

上面匿名内部类可以使用lambda表达式的形式进行替代

1
2
3
4
ini复制代码 String dateStr2 = ((KiteFunction<LocalDateTime, String, String>) (localDateTime, pattern) -> {
             DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
             return localDateTime.format(dateTimeFormatter);
        }).run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");

Stream流

Stream流中大量运用了方法引用和lambda表达式,并提供了很多简单易用封装好的工具API,使用这些API我们可以快速高效地对复杂数据进行处理

  1. flatMap 对多维数据进行扁平化处理
  2. peek函数类似于foreach函数,区别是foreach函数执行后没有返回结果,而peek返回了Stream,但是如果执行peek函数后,没有对数据进行收集操作,peek函数内部的方法不会被执行
  3. reduce函数,对数据进行累加操作
1
2
3
4
5
6
7
8
9
10
java复制代码 //reduce方法定义
 Optional<T> reduce(BinaryOperator<T> accumulator);
 //实际用法,对List<Integer>中的元素进行累加求和
 Integer result = stream.reduce((x, y) -> x + y).get();
 x是Stream中的第一个元素,y是第二个元素
 //reduce重载方法定义
 T reduce(T identity, BinaryOperator<T> accumulator);
 //实际用户,累加求和时先赋一个初始值
 stream.reduce(100, (x, y) -> x + y)
 累加时,identity会作为第一个参数

Stream流程常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码 Trader raoul = new Trader("Raoul", "Cambridge");
 Trader mario = new Trader("Mario", "Milan");
 Trader alan = new Trader("Alan", "Cambridge");
 Trader brian = new Trader("Brian", "Cambridge");
 ​
 List<Transaction> transactions = Arrays.asList(
     new Transaction(brian, 2011, 300),
     new Transaction(raoul, 2012, 1000),
     new Transaction(raoul, 2011, 400),
     new Transaction(mario, 2012, 710),
     new Transaction(mario, 2012, 700),
     new Transaction(alan, 2012, 950)
 );
 //1、找到2011年所有的交易,并对交易值进行倒叙排序
 List<Transaction> collect = transactions.stream().filter(t -> t.getYear() == 2011).sorted(Comparator.comparing(Transaction::getValue).reversed()).collect(Collectors.toList());
 System.out.println(collect);
 //2、交易员都在哪些不同的城市工作过
 transactions.stream().map(Transaction::getTrader).map(Trader::getCity).distinct().forEach(e->System.out.println(e));
 //3、判断有无交易员在米兰城市待过
 boolean isExit = transactions.stream().map(Transaction::getTrader).map(Trader::getCity).anyMatch(e -> "Milan".equals(e));
 System.out.println(isExit);
 //4、打印生活在剑桥的交易员的所有交易金额
 long sum = transactions.stream().filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity())).collect(Collectors.summarizingInt(Transaction::getValue)).getSum();
 System.out.println("剑桥总交易金额"+sum);
 //5、所有的交易中,交易值最大的是
 Transaction transaction = transactions.stream().max(Comparator.comparing(Transaction::getValue)).get();
 System.out.println(transaction);

总结

我们平时说的函数式编程,函数即方法引用,式即Lambda表达式
Stream流可以提高我们的编程效率,但是也需要合理的使用StreamAPI,涉及到复杂的业务逻辑时需要写好注释

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 中的监控与管理原理概述

发表于 2021-11-30

点赞再看,动力无限。Hello world : ) 微信搜「 程序猿阿朗 」。

本文 Github.com/niumoo/Java… 和 程序猿阿朗博客 已经收录,有很多知识点和系列文章。

当前文章属于Java 性能分析优化系列文章,点击可以查看所有文章。

这篇文章是 Java 性能分析监控与优化系列的第一篇文章,原本是计划系统的介绍 Java 性能分析方式和流行的监控工具,但是提笔之后意识到,只介绍分析方式和监控工具过于浅尝辄止了。如果只会使用某个工具而不知道背后的实现原理,总觉得有种陌生感,我想你们也是一样,所以多了这篇文章。

文章目录

Java SE 监控管理功能

这篇文章介绍 Java Standard Edition(Java SE)平台提供的监控和管理技术 - JMX(Java Management Extensions) 技术。

Java SE 平台本身就提供了用于监控和管理服务的实用性功能模块,按功能来说主要分为下面四类:

  • Java 监控和管理 API
  • Java 虚拟机检测
  • Java 管理扩展技术(JMX)
  • Java 监控和管理的工具

这篇文章会介绍这四个部分的相关知识,旨在了解 Java SE 监控与管理的相关功能,对其中的相关概念有个理解。

Java 监控和管理 API

Java SE 中包含了用于监控和管理的(java.lang.management)API,通过这些 API 可以实现应用程序的自我监控,此 API 主要提供了以下信息的访问:

  • 类加载相关。
  • JVM 相关,如运行时间、系统环境变量、用户输入参数。
  • 线程相关,如线程状态,线程的统计信息、线程的堆栈等。
  • 内存使用情况。
  • GC 情况。
  • 死锁检测。
  • 操作系统信息。

下图是 Java 17 中的 java.management 模块。

java.lang.management

JConsole 就是通过访问这些管理 API 提供的数据,绘制了监控的界面版。

Java 虚拟机监测

上面说到 Java SE 中已经内置了开箱即用的监控和管理功能,通过这些功能可以实现程序的自我监测,Java 默认已经实现了对 Java 虚拟机相关信息的监测,在 Java 监控和管理 API 部分也列举了 API 可以监测的部分内容,那么怎么使用呢?

下面通过一个简单的示例,演示如何通过监控管理 API 获取系统信息、编译器信息、内存信息以及垃圾收集器信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码package com.wdbyte;

import java.lang.management.CompilationMXBean;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryManagerMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.OperatingSystemMXBean;
import java.util.List;
import java.util.stream.Collectors;

public class JavaManagement {

public static void main(String[] args) {
OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
String osName = operatingSystemMXBean.getName();
String osVersion = operatingSystemMXBean.getVersion();
int processors = operatingSystemMXBean.getAvailableProcessors();
System.out.println(String.format("操作系统:%s,版本:%s,处理器:%d 个", osName, osVersion, processors));

CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean();
String compilationMXBeanName = compilationMXBean.getName();
System.out.println("编译系统:" + compilationMXBeanName);

MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
long max = heapMemoryUsage.getMax();
long used = heapMemoryUsage.getUsed();
System.out.println(String.format("使用内存:%dMB/%dMB", used / 1024 / 1024, max / 1024 / 1024));

List<GarbageCollectorMXBean> gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans();
String gcNames = gcMXBeans.stream()
.map(MemoryManagerMXBean::getName)
.collect(Collectors.joining(","));
System.out.println("垃圾收集器:" + gcNames);
}
}

运行时指定了内存为 100MB(-Xms100M -Xmx100M),得到如下结果。

1
2
3
4
she复制代码操作系统:Mac OS X,版本:11.6,处理器:12 个
编译系统:HotSpot 64-Bit Tiered Compilers
使用内存:2MB/100MB
垃圾收集:G1 Young Generation,G1 Old Generation

:::tip 注意

细看代码可以发现其中很多类都是以 MXBean 结尾,这是什么意思呢?

:::

Java 管理扩展技术(JMX)

在 Java 虚拟机监测中的代码示例中,可以看到很多命名以 MXBean 结尾的类,这里已经涉及到了 JMX(Java Management Extensions) 技术。

JMX 技术提供了一种简单、标准的方式来管理资源,如操作系统、虚拟机信息、内存状态、线程信息等,这些统称为被管理的资源。而且 JMX 是可以动态的,所以可以使用 JMX 技术来监测和管理各种资源。可以使用 JMX 技术来监测 Java 虚拟机状态,也可以使用 JMX 技术构建自己的需要管理的资源。

JMX 技术只有资源定义那么简单吗?不是的。JMX 规范了 Java 中资源定义的方式、资源管理的方式、监控和管理的体系结构、具体实现的设计模式、监控和管理的相关 API 以及用于网络的远程监控服务(RMI),这一系列功能统称为 JMX 技术。是 Java SE 平台的标准部分。

上面多次提到了管理资源,那么如何定义一个资源呢?JMX 技术给出了资源定义的体系结构和设计模式,在 JMX 中,通过定义一个被称为 MBean 或 MXBean 的 Java 对象来表示要管理指定的资源,资源定义的 Java 类名必须以 MBean 或 MXBean 结尾。

下图是 Java 17 中的以 MXBean 结尾的资源定义类,通过命名可以看出每个类代表了什么资源。

Java 中的 MXbean

这篇文章主要是介绍 Java SE 中的监控与管理功能,让大家对 Java 中的监控与管理背后的原理和概念有一个具体的认识,所以 MBean 和 MXBean 的具体设计实现方式不是这篇文章的重点,这里不过多介绍,会放到下一篇独立的 JMX 技术文章中介绍。

Java 监控和管理的工具

JMX 技术中提到 JMX 不仅提供了监控和管理的 API ,还提供了用于网络远程管理的服务,可以使用 JMX 相关监控管理工具,通过网络远程连接到正在运行 Java 虚拟机,监控其运行状态,Java 中集成的 jconsole 就是这样一款工具。

本地随意启动一个可以持续运行的 Java 程序用作被监测对象,如果你已经配置好 Java 环境变量,可以直接通过 jconsole 启动工具。

1
shell复制代码 $ jconsole

启动后的 jconsole 已经列出了本地正在运行的 Java 程序,选择自己想要监测的进行进行监测。

Jconsole 界面

连接成功后可以看到当前 Java 进程的资源占用情况。

JConsole 监控

在 MBean 页面中,可以看到各种已经被定义的资源的具体情况。

Jconsole MBean 情况

Jconsole 是一款强大的图形界面 JMX 管理工具,不仅可以连接本地 Java 程序,还可以通过网络监控远程的 Java 程序运行状态,不过不是此篇文章重点,不在详细描述。

参考:

  • docs.oracle.com/en/java/jav…
  • docs.oracle.com/en/java/jav…

当前文章属于Java 性能分析优化系列文章,点击可以查看所有文章。

文章持续更新,可以微信搜一搜「 程序猿阿朗 」或访问「程序猿阿朗博客 」第一时间阅读。本文 Github.com/niumoo/Java… 已经收录,有很多知识点和系列文章,欢迎Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

netty(十七)Netty提升 - 你不知道的Shara

发表于 2021-11-30

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」

一、什么是@Sharable

首先我们看下如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
scss复制代码public class Server {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
//引入编解码器
ch.pipeline().addLast(new MessageCodec());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//打印消息内容
System.out.println(msg);
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}

在上述的代码当中,有三个handler,而这三个handler是针对每一个SocketChannel 的,当连接数很多的时候,可能需要创建大量的handler实例,占据大量的内存空间。

那么是否能够将handler提出来作为公共使用呢?

当然是可以的,但是要考虑两种handler是否存在线程安全问题。

这里就可以使用这个注解@Sharable来表示。

其注释的含义如下所示:

表示可以将带注释的ChannelHandler的同一实例多次添加到一个或多个ChannelPipeline ,而不会出现竞争条件。
如果未指定此注解,则每次将其添加到管道时都必须创建一个新的处理程序实例,因为它具有成员变量等非共享状态。

二、使用@Sharable

首先我们看下前一章节当中自己创建的编解码器,添加上该注解后,是否能够正常使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ini复制代码/**
* @description: 消息编解码器
* @author:weirx
* @date:2021/11/16 14:49
* @version:3.0
*/
@ChannelHandler.Sharable
public class MessageCodec extends ByteToMessageCodec<Message> {

@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message msg, ByteBuf out) throws Exception {
// 4 字节的魔数
out.writeBytes(new byte[]{1, 2, 3, 4});
// 1 字节的版本,
out.writeByte(1);
// 1 字节的序列化方式 0:json
out.writeByte(0);
// 1 字节的指令类型
out.writeByte(msg.getMessageType());
// 4 个字节的请求序号
out.writeInt(msg.getSequenceId());
// 无意义,对齐填充,使其满足2的n次方
out.writeByte(0xff);
// 获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
// 长度
out.writeInt(bytes.length);
// 写入内容
out.writeBytes(bytes);
}

@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes, 0, length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
System.out.println("decode:" + magicNum + "," + version + "," + serializerType + "," + messageType + "," + sequenceId + "," + length);
System.out.println("decode:" + message);
out.add(message);
}
}

添加上之后就㞏正常启动客户端了。分析一下问题,看MessageCodec 继承了ByteToMessageCodec,我们跟踪源码进去看看。因为我们的这个类没有构造方法,所以直接去看父类ByteToMessageCodec的构造方法:

1
2
3
4
5
ini复制代码    protected ByteToMessageCodec(boolean preferDirect) {
ensureNotSharable();
outboundMsgMatcher = TypeParameterMatcher.find(this, ByteToMessageCodec.class, "I");
encoder = new Encoder(preferDirect);
}

有一个 ensureNotSharable()的方法,直译过来就是确保不是可共享的。

1
2
3
4
5
csharp复制代码    protected void ensureNotSharable() {
if (isSharable()) {
throw new IllegalStateException("ChannelHandler " + getClass().getName() + " is not allowed to be shared");
}
}

如果是,就抛出异常。

其实在ByteToMessageCodec这个类的注释上就表明了这个类的子类必须不是可共享的。如下所示。

1
2
3
4
5
6
7
8
9
10
scala复制代码/**
* A Codec for on-the-fly encoding/decoding of bytes to messages and vise-versa.
*
* This can be thought of as a combination of {@link ByteToMessageDecoder} and {@link MessageToByteEncoder}.
*
* Be aware that sub-classes of {@link ByteToMessageCodec} <strong>MUST NOT</strong>
* annotated with {@link @Sharable}.
* 子类ByteToMessageCodec必须不@Sharable注解。
*/
public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler

那么想让我们的handler变成可共享的要怎么办 ?

第一、只要保证我们的handler没有成员变量等非共享状态。

第二、修改继承的父类,对于编解码器类,不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制。

第三、可以使用MessageToMessageCodec 父类作为父类。

满足以上就可以让我们自己的handler使用@Sharable注解了。


本章就到此为止,有用的话,点个赞再走吧~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringCloud升级之路20200x版-44避免

发表于 2021-11-30

这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战

本系列代码地址:github.com/JoJoTec/spr…

我们在这一节我们将继续讲解避免链路信息丢失做的设计,主要针对获取到现有 Span 之后,如何保证每个 GlobalFilter 都能保持链路信息。首先,我们自定义 Reactor 的核心 Publisher 即 Mono 和 Flux 的工厂,将链路信息封装进去,保证由这个工厂生成的 Mono 和 Flux,都是只要是这个工厂生成的 Mono 和 Flux 之间无论怎么拼接都会保持链路信息的:

自定义 Mono 和 Flux 的工厂

公共 Subscriber 封装,将 reactor Subscriber 的所有关键接口,都检查当前上下文是否有链路信息,即 Span,如果没有就包裹上,如果有则直接执行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
csharp复制代码public class TracedCoreSubscriber<T> implements Subscriber<T>{
private final Subscriber<T> delegate;
private final Tracer tracer;
private final CurrentTraceContext currentTraceContext;
private final Span span;

TracedCoreSubscriber(Subscriber<T> delegate, Tracer tracer, CurrentTraceContext currentTraceContext, Span span) {
this.delegate = delegate;
this.tracer = tracer;
this.currentTraceContext = currentTraceContext;
this.span = span;
}

@Override
public void onSubscribe(Subscription s) {
executeWithinScope(() -> {
delegate.onSubscribe(s);
});
}

@Override
public void onError(Throwable t) {
executeWithinScope(() -> {
delegate.onError(t);
});
}

@Override
public void onComplete() {
executeWithinScope(() -> {
delegate.onComplete();
});
}

@Override
public void onNext(T o) {
executeWithinScope(() -> {
delegate.onNext(o);
});
}

private void executeWithinScope(Runnable runnable) {
//如果当前没有链路信息,强制包裹
if (tracer.currentSpan() == null) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(this.span.context())) {
runnable.run();
}
} else {
//如果当前已有链路信息,则直接执行
runnable.run();
}
}
}

之后分别定义所有 Flux 的代理 TracedFlux,和所有 Mono 的代理 TracedMono,其实就是在 subscribe 的时候,用 TracedCoreSubscriber 包装传入的 CoreSubscriber:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
scala复制代码public class TracedFlux<T> extends Flux<T> {
private final Flux<T> delegate;
private final Tracer tracer;
private final CurrentTraceContext currentTraceContext;
private final Span span;

TracedFlux(Flux<T> delegate, Tracer tracer, CurrentTraceContext currentTraceContext, Span span) {
this.delegate = delegate;
this.tracer = tracer;
this.currentTraceContext = currentTraceContext;
this.span = span;
}

@Override
public void subscribe(CoreSubscriber<? super T> actual) {
delegate.subscribe(new TracedCoreSubscriber(actual, tracer, currentTraceContext, span));
}
}

public class TracedMono<T> extends Mono<T> {
private final Mono<T> delegate;
private final Tracer tracer;
private final CurrentTraceContext currentTraceContext;
private final Span span;

TracedMono(Mono<T> delegate, Tracer tracer, CurrentTraceContext currentTraceContext, Span span) {
this.delegate = delegate;
this.tracer = tracer;
this.currentTraceContext = currentTraceContext;
this.span = span;
}

@Override
public void subscribe(CoreSubscriber<? super T> actual) {
delegate.subscribe(new TracedCoreSubscriber(actual, tracer, currentTraceContext, span));
}
}

定义工厂类,使用请求 ServerWebExchange 和原始 Flux 创建 TracedFlux,以及使用请求 ServerWebExchange 和原始 Mono 创建 TracedMono,并且 Span 是通过 Attributes 获取的,根据前文的源码分析我们知道,这个 Attribute 是通过 TraceWebFilter 放入 Attributes 的。由于我们只在 GatewayFilter 中使用,一定在 TraceWebFilter 之后 所以这个 Attribute 一定存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码@Component
public class TracedPublisherFactory {
protected static final String TRACE_REQUEST_ATTR = Span.class.getName();

@Autowired
private Tracer tracer;
@Autowired
private CurrentTraceContext currentTraceContext;

public <T> Flux<T> getTracedFlux(Flux<T> publisher, ServerWebExchange exchange) {
return new TracedFlux<>(publisher, tracer, currentTraceContext, (Span) exchange.getAttributes().get(TRACE_REQUEST_ATTR));
}

public <T> Mono<T> getTracedMono(Mono<T> publisher, ServerWebExchange exchange) {
return new TracedMono<>(publisher, tracer, currentTraceContext, (Span) exchange.getAttributes().get(TRACE_REQUEST_ATTR));
}
}

公共抽象 GlobalFilter - CommonTraceFilter

我们编写所有我们后面要实现的 GlobalFilter 的抽象类,这个抽象类的主要功能是:

  • 保证继承这个抽象类的 GlobalFilter 本身以及拼接的链路中,是有链路信息的,其实就是保证它 filter 返回的 Mono 是由我们上面实现的 Factory 生成的即可。
  • 不同 GlobalFilter 之间需要排序,有顺序的执行,这个通过实现 Ordered 接口即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
kotlin复制代码package com.github.jojotech.spring.cloud.apigateway.filter;

import com.github.jojotech.spring.cloud.apigateway.common.TraceWebFilterUtil;
import com.github.jojotech.spring.cloud.apigateway.common.TracedPublisherFactory;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;


/**
* 所有 filter 的子类
* 主要保证 span 的完整性,在某些情况下,span 会半途停止,导致日志中没有 traceId 和 spanId
* 参考:https://github.com/spring-cloud/spring-cloud-sleuth/issues/2004
*/
public abstract class AbstractTracedFilter implements GlobalFilter, Ordered {
@Autowired
protected Tracer tracer;
@Autowired
protected TracedPublisherFactory tracedPublisherFactory;
@Autowired
protected CurrentTraceContext currentTraceContext;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Mono<Void> traced;
if (tracer.currentSpan() == null) {
try (CurrentTraceContext.Scope scope = this.currentTraceContext
.maybeScope(((Span) exchange.getAttributes().get(TraceWebFilterUtil.TRACE_REQUEST_ATTR))
.context())) {
traced = traced(exchange, chain);
}
}
else {
//如果当前已有链路信息,则直接执行
traced = traced(exchange, chain);
}
return tracedPublisherFactory.getTracedMono(traced, exchange);
}

protected abstract Mono<Void> traced(ServerWebExchange exchange, GatewayFilterChain chain);
}

这样,我们就可以基于这个抽象类去实现需要定制的 GlobalFilter 了

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Springboot系列(十一) mybatis-plus实

发表于 2021-11-30

👨‍🎓作者:bug菌

✏️博客:CSDN、掘金、infoQ、51CTO等

🎉简介:CSDN博客专家,C站历届博客之星Top50,掘金/InfoQ/51CTO等社区优质创作者,全网合计8w粉+,对一切技术感兴趣,重心偏Java方向;硬核公众号「 猿圈奇妙屋」,欢迎小伙伴们的加入,一起秃头,一起变强。

..

✍️温馨提醒:本文字数:1999字, 阅读完需:约 5 分钟

嗨,家人们,我是bug菌呀,我又来啦。今天我们来聊点什么咧,OK,接着为大家更《springboot零基础入门教学》系列文章吧。希望能帮助更多的初学者们快速入门!

小伙伴们在批阅文章的过程中如果觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐+关注👨‍🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️

一、前言🔥

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

正常情况下我们在开发系统的时候都是使用一个数据源,但是由于有些项目同步数据的时候不想造成数据库io消耗压力过大,便会一个项目对应多个数据源,即就会有个问题,Springboot 配置db都是默认加载数据源连接,连接池默认配置,但是配置多个数据库url,这该怎么实现呢?

不用担心,我们就是为了解决而写的,不用复杂的实现方式,什么Springboot+mybatis在配置文件中配置多个数据源,然后mapper指定连接配置等,不,不需要,我嫌太麻烦了,今天我就要给你们安利它: **dynamic-datasource-spring-boot-starter,一个基于springboot的快速集成多数据源的启动器,开箱即用,超级方便。**接下来我就为大家一一讲解,虽然配置很简单,但是坑也比较多,bug菌都为大家给淌过啦,你们就直接直接拿去用即可!

二、dynamic-datasource-spring-boot-starter

一、简介

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x。

二、特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密 ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 **基于seata的分布式事务方案。
  • 提供 本地多数据源事务方案。

三、使用方法

1、老规矩,先引依赖包。引入dynamic-datasource-spring-boot-starter;

1
2
3
4
5
6
xml复制代码<!--配置多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>

2、配置数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver #3.2.0开始支持SPI可省略此配置
url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
slave_1:
driver-class-name: com.mysql.cj.jdbc.Driver #3.2.0开始支持SPI可省略此配置
url: jdbc:mysql://127.0.0.1:3306/test_db2?setUnicode=true&characterEncoding=utf8
username: root
password: 123456

拓展:同时也支持多主多从,多种不同库及混合配置。

3、实战演示:

我们先来指定配置环境:

配置好配置环境后,先启动下项目,查看库是否都正常连接。如下控制台启动打印的日志,表示两库都正常连接。

接着,我们先创建两个实体,分别对应db1和db2的user表:

)

接着写一个controller类;

接着接口实现层:

这里我就为大家讲解一下了。

数据源切换,使用的是提供的 @DS 注解。

其中@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。(所以你们看上边,注解在类上使用的是主库,但是基于下边某个方法就是制定了从库,但是我这样放一起是为了给大家看比较,一般会直接分类写,不会一个类上出现多个数据源。)

注:@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称。

4、访问接口查看结果。

先是访问接口1:localhost:8888/dbUser/query-users-for-db-one

如下请求结果,大家请看,成功拿到了db1库中的数据。

再请求下接口2:localhost:8888/dbUser/query-users-for-db-two

如下请求结果,大家请看,成功拿到了db2库中的数据。

再做个测试,我们把db1与db2中的数据同时返回,看看能否成功:

我们先写两接口,然后分别将数据用map返回:

请求结果如下:

然后给大家看下数据库数据,以免被大家说是同一个库中的数据。

好啦,以上就是同mysql数据源的配置流程及实例演示啦,如果还有啥不清楚的小伙伴,欢迎下方留言。接着就是针对后边两种配置方式,就自行尝试啦,看上去都是一样的。

有需求的小伙伴,看完之后,使用起来是不是很简单,压根不需要用传统的mybatis配置多个连接器,mybatis-plus都帮我们封装好啦,开箱即用。

… …

OK,以上就是这期所有的内容啦,如果有任何问题欢迎评论区批评指正,咱们下期见。

五、往期推荐

  • springboot系列(一):如何创建springboot项目及启动
  • springboot系列(二):yaml、properties两配置文件介绍及使用
  • springboot系列(三):多环境切换,实例演示
  • springboot系列(四):stater入门
  • springboot系列(五):史上最最最全springboot常用注解
  • springboot系列(六):mysql配置及数据库查询
  • springboot系列(七):如何通过mybatis-plus实现接口增删改查
  • springboot系列(八):mybatis-plus之条件构造器使用手册
  • springboot系列(九):mybatis-plus之如何自定义sql
  • springboot系列(十):mybatis之xml映射文件>、<=等特殊符号写法
  • springboot系列(十一):实现多数据源配置,开箱即用
  • springboot系列(十二):如何实现邮件发送提醒,你一定得会(准备篇)
  • springboot系列(十三):如何实现发送普通邮件?你一定得会
  • springboot系列(十四):如何实现发送图片、doc文档等附件邮件?你一定得会
  • springboot系列(十五):如何实现静态邮件模板发送?你一定得会
  • springboot系列(十六):如何实现发送邮件提醒,附完整源码
  • springboot系列(十七):集成在线接口文档Swagger2
  • springboot系列(十八):如何Windows安装redis?你玩过么
  • springboot系列(十九):如何集成redis?不会我教你
  • springboot系列(二十):如何通过redis实现手机号验证码功能
  • … …

文末🔥

如果还想要学习更多,小伙伴们可关注bug菌专门为大家创建的专栏《springboot零基础入门教学》,从无到有,从零到一!希望能帮助到更多小伙伴们。

我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

感谢认真读完我博客的铁子萌,在这里呢送给大家一句话,不管你是在职还是在读,绝对终身受用。

时刻警醒自己:

抱怨没有用,一切靠自己;

想要过更好的生活,那就要逼着自己变的更强,生活加油!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux小技巧:注册为服务,设置开机启动 linux小技巧

发表于 2021-11-30

linux小技巧:注册为服务,设置自启动

「这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战」

方法1. System V Init服务

System V Init服务都在目录/etc/init.d/下面。而实际是一个软连接

image.png

软连接连接到/etc/rc.d/init.d/

image.png

运行级别(run level)

Init进程是系统启动之后的第一个用户进程,所以它的pid(进程编号)始终为1。init进程上来首先做的事是去读取/etc/目录下inittab文件中initdefault id值,这个值称为运行级别(run-level)。它决定了系统启动之后运行于什么级别。运行级别决定了系统启动的绝大部分行为和目的。这个级别从0到6 ,具有不同的功能。不同的运行级定义如下:

  • 0 - 停机(千万别把initdefault设置为0,否则系统永远无法启动)
  • 1 - 单用户模式
  • 2 - 多用户,没有 NFS
  • 3 - 完全多用户模式(标准的运行级)
  • 4 - 系统保留的
  • 5 - X11 (x window)
  • 6 - 重新启动 (千万不要把initdefault 设置为6,否则将一直在重启 )

/etc/rc.d/与/etc/rc.d/init.d的关系

/etc/rc.d/init.d这个目录下的脚本就类似与windows中的注册表,在系统启动的时候执行。

在决定了系统启动的run level之后,/etc/rc.d/rc这个脚本先执行。在RH9和FC7的源码中它都是一上来就check_runlevel(),知道了运行级别之后,对于每一个运行级别,在rc.d下都有一个子目录分别是rc0.d,rc1.d ….. rc6.d。

每个目录下都是到init.d目录的一部分脚本一些链接。每个级别要执行哪些服务就在相对应的目录下,比如级别5要启动的服务就都放在rc5.d下,但是放在这个rc5.d下的都是一些链接文件,链接到init.d中相对应的文件,真正干活到init.d里的脚本。

  1. 这些链接文件前面为什么会带一个Kxx或者Sxx呢?
  • K的表示停止(Kill)服务,会自动给脚本带上stop参数
  • S表示开启(Start)服务,会自动给脚本带上start参数
  1. K和S后面带的数字呢?
  • 用来排序,就是决定这些脚本执行的顺序,数值小的先执行,数值大的后执行
  • 很多时候这些执行顺序是很重要的,比如要启动Apache服务,就必须先配置网络接口
  1. 无意中发现同一个服务带S的和带K的链接到init.d之后是同一个脚本
  • S给和K还分别给init.d下面的脚本传递了start和stop的参数。
    • 传S时相当于执行了/etc/rc.d/init.d/xxx start
    • 传K就相当于/etc/rc.d/init.d/xxx stop

实践

1. 创建脚本

vi /app/self-start/myApp.sh

脚本定义了接收系统启动时传过来的参数该如何运行程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
sh复制代码#!/bin/sh
# chkconfig: 2345 85 15
# description:ljw test auto_run

##第一行,告诉系统使用的shell

##第二行,2345代表在设置在那个level中是on的,如果一个都不想on,那就写一个横线"-",85和15, 后面两个数字代表S和K的默认排序号 ,
##告诉chkconfig程序,需要在rc2.d~rc5.d目录下,创建名字为S80myApp的文件连接, 第一个字符是S,系统在启动的时候,运行脚本myApp

##注意上面的三行中,第二,第三行是必须的,否则在运行chkconfig –add auto_run时,会报错。
### 85 数字越小 启动优先级别越高
### 15 数字越小 关闭优先级别越高
### description描述可以在开机日志中查找:cat /var/log/messages | grep ljw
#程序名
RUN_NAME="myApp"

#jar 位置
JAVA_OPTS=/app/self-start/$RUN_NAME.jar
LOG_OPTS=/app/self-start/$RUN_NAME-log.log

echo $JAVA_OPTS
echo $LOG_OPTS

#开始方法
start() {

## 判断是否已经启动
isRun=`ps -ef | grep $JAVA_OPTS | grep -v grep | awk '{print $2}'`
if [ $isRun ]
then
echo "$RUN_NAME is running!! Do not operate"
else
nohup java -jar $JAVA_OPTS > $LOG_OPTS &
echo "$RUN_NAME started success!"
fi

}

#结束方法
stop() {
echo "stopping $RUN_NAME ..."
####赋值=号左边不能有空格,深坑啊。。。
aa=`ps -ef | grep $JAVA_OPTS | grep -v grep | grep -v stop | awk '{print $2}'`
echo kill: $aa
kill -9 $aa
}

case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Userage: $0 {start|stop|restart}"
exit 1
esac

2. 赋予脚本和程序权限

chmod +x /app/self-start/myApp.sh

chmod +x /app/self-start/myApp.jar

3. 在\etc\init.d 下创建脚本软连接到脚本

sudo ln -s /app/self-start/myApp.sh /etc/init.d/myApp

4. 添加为系统服务

chkconfig --add myApp

查看是否添加

chkconfig --list

image.png

5. 设置开机自启动

chkconfig myApp on

6. 服务命令

开启服务

service myApp start

停止服务

service myApp stop

重启服务

service myApp restart

7. 查看服务器重启日志

服务器重启可以重启配置的服务,配置的服务的description描述可以在开机日志中查找。

cat /var/log/messages | grep ljw

报错:服务不支持 chkconfig

请注意检查脚本的前面,是否有完整的两行:

1
2
js复制代码# chkconfig: 2345 85 15
# description:ljw test auto_run

在脚本前面这两行是不能少的,否则不能chkconfig命令会报错误。

这是根据脚本配置的chkconfig自动生成的软连接:2345级别都是S85:开启状态排序85
image.png

如果运行chkconfig老是报错,如果脚本没有问题,我建议,直接在rc0.d~rc6.d下面创建到脚本的文件连接来解决,原理都是一样的。

方法2. Systemd服务(推荐)

Systemd作为后起之秀,功能更加强大,支持的命令和参数也更多,Unit 是 systemd 进行任务管理的基本单位

Systemd中Unit文件的存放目录

  1. /etc/systemd/system:系统或用户自定义的配置文件
  2. /run/systemd/system:软件运行时生成的配置文件
  3. /usr/lib/systemd/system:系统或第三方软件安装时添加的配置文件。(也是lib/systemd/system;centos8中,因为/lib目录已经变成了/usr/lib的一个符号链接)
  • Unit 文件按照 Systemd 约定,被放置指定的三个系统目录之一即可
  • 三个目录是有优先级的越上面的优先级越高
  • 在三个目录中有同名文件的时候,只有优先级最高的目录里的文件会被使用
  • Systemd 默认从目录 /etc/systemd/system/ 读取配置文件,里面存放的大部分文件都是符号链接,指向目录 /usr/lib/systemd/system/
  • /etc/systemd/system目录下通常不放置unit文件本身,而是放置/usr/lib/systemd/system中unit文件的符号链接
1
2
3
4
5
6
7
8
sh复制代码# 查看所有单元
$ systemctl list-unit-files

# 查看所有 Service 单元
$ systemctl list-unit-files --type service

# 查看所有 Timer 单元
$ systemctl list-unit-files --type timer

unit的类型

  • Service unit:系统服务
  • Target unit:多个 Unit 构成的一个组
  • Device Unit:硬件设备
  • Mount Unit:文件系统的挂载点
  • Automount Unit:自动挂载点
  • Path Unit:文件或路径
  • Scope Unit:不是由 Systemd 启动的外部进程
  • Slice Unit:进程组
  • Socket Unit:进程间通信的 socket
  • Swap Unit:swap 文件
  • Timer Unit:定时器
  • snapshot Unit:表示由 systemctl snapshot 命令创建的 Systemd Units 运行状态快照

Service unit的文件格式说明

service 类型的 unit 代表一个后台服务进程。接下来我们就详细的介绍如何配置 service 类型的 unit。

下面我们先来看一个简单的服务配置:

cat /usr/lib/systemd/system/firewalld.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sh复制代码[Unit]
Description=firewalld - dynamic firewall daemon
Before=network-pre.target
Wants=network-pre.target
After=dbus.service
After=polkit.service
Conflicts=iptables.service ip6tables.service ebtables.service ipset.service
Documentation=man:firewalld(1)

[Service]
EnvironmentFile=-/etc/sysconfig/firewalld
ExecStart=/usr/sbin/firewalld --nofork --nopid $FIREWALLD_ARGS
ExecReload=/bin/kill -HUP $MAINPID
# supress to log debug and error output also to /var/log/messages
StandardOutput=null
StandardError=null
Type=dbus
BusName=org.fedoraproject.FirewallD1
KillMode=mixed

[Install]
WantedBy=multi-user.target
Alias=dbus-org.fedoraproject.FirewallD1.service

服务类型的配置文件名称必须以 .service 结尾。

Unit

[Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系,包括在什么服务之后才启动此 unit 之类的设置。

  • Description:关于该unit的简短描述
  • Documentation:文档地址
  • Requires:当前 Unit 依赖的其他 Unit,如果它们没有运行,当前 Unit 会启动失败.(强依赖)
  • Wants:与当前 Unit 配合的其他 Unit,如果它们没有运行,当前 Unit 不会启动失败.(弱依赖)
  • BindsTo:与Requires类似,它指定的 Unit 如果退出,会导致当前 Unit 停止运行
  • Before:表示本服务需要在xxx服务启动之前执行
  • After:表示本服务需要在xxx服务启动之后执行
  • Conflicts:这里指定的 Unit 不能与当前 Unit 同时运行
  • Condition…:当前 Unit 运行必须满足的条件,否则不会运行
  • Assert…:当前 Unit 运行必须满足的条件,否则会报启动失败

所有的启动设置之前,都可以加上一个连词号(-),表示抑制错误,即发生错误的时候,不影响其他命令的执行。

比如,EnvironmentFile=-/etc/sysconfig/sshd(注意等号后面的那个连词号),就表示使/etc/sysconfig/sshd文件不存在,也不会抛出错误。

Service

[Service]区块用来 Service 的配置,只有 Service 类型的 Unit 才有这个区块。

不同的 unit 类型就得要使用相对应的设置项目,比如 timer 类型的 unit 应该是 [Timer];socket 类型的 unit 应该是 [Socket];服务类型的 unit 就是 [Service],这个项目内主要在规范服务启动的脚本、环境配置文件文件名、重新启动的方式等等。

  • Type:定义启动时的进程行为它有以下几种值
    • Type=simple:默认值,执行ExecStart指定的命令,启动主进程,ExecStart字段启动的进程为主进程
    • Type=forking:以 fork 方式从父进程创建子进程,创建后父进程会立即退出,子进程将作为该服务的主进程继续运行,这是传统UNIX守护进程的经典做法。(jar服务用这个)
    • Type=oneshot:一次性进程,Systemd 会等它执行完,才继续启动其他服务
    • Type=dbus:类似于simple,但会等待 D-Bus 信号后启动
    • Type=notify:当前服务启动完毕,会通知Systemd,再继续往下执行
    • Type=idle:类似于simple,但是要等到其他任务都执行完,才会启动该服务。一种使用场合是为让该服务的输出,不与其他服务的输出相混合
  • User :使用哪个用户环境变量来正确运行该服务
  • ExecStart:启动当前服务的命令:不能接受 <, >, >>, |, & 等特殊字符,很多的 bash 语法也不支持。所以,要使用这些特殊的字符时,最好直接写入到脚本里面去!
  • ExecStartPre:启动当前服务之前执行的命令
  • ExecStartPost:启动当前服务之后执行的命令
  • ExecReload:重启当前服务时执行的命令
  • ExecStop:停止当前服务时执行的命令
  • ExecStopPost:停止当其服务之后执行的命令
  • RestartSec:自动重启当前服务间隔的秒数
  • Restart:定义何种情况 Systemd 会自动重启当前服务
    • no(默认值):退出后不会重启
    • on-success: 只有正常退出时(退出状态码为0),才会重启
      • 正常退出:退出码为”0”,或者进程收到 SIGHUP, SIGINT, SIGTERM, SIGPIPE 信号并且退出码符合 SuccessExitStatus= 的设置。
    • on-failure:非正常退出时(退出状态码非0),包括被信号终止和超时,才会重启
    • on-abnormal:只有被信号终止和超时,才会重启
    • on-abort:只有在收到没有捕捉到的信号终止时,才会重启
    • on-watchdog: 超时退出,才会重启
    • always: 不管是什么退出原因,总是重启(当进程是由于 systemd 的正常操作(例如 systemctl stop|restart)而被停止时, 该服务不会被重新启动。即使用kill -9还是会重启的)

image.png

  • TimeoutSec:定义 Systemd 停止当前服务之前等待的秒数
  • SuccessExitStatus :定义附加的进程”正常退出”状态,如:SuccessExitStatus=1 2 8 SIGKILL ;表示当进程的退出码是 1, 2, 8 或被 SIGKILL 信号终止时,都可以视为”正常退出”
  • PrivateTmp: 否使用私有的tmp目录
  • KillMode:定义 Systemd 如何停止服务,可以设置的值如下:
    • control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
    • process:只杀主进程
    • mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
    • none:没有进程会被杀掉,只是执行服务的 stop 命令
  • Environment:指定环境变量,可以使用多次
1
2
3
4
sh复制代码[Service]
# Client Env Vars
Environment=XXX_A=A
Environment=XXX_B=B
  • EnvironmentFile:环境参数文件
    • 1.脚本 可以把下面的内容保存到文件 testenv 中:
      • AAA_IPV4_ANCHOR_0=X.X.X.X
    • 2.声明环境文件 EnvironmentFile=/testenv
    • 3.使用:ExecStart=/xxx xx${AAA_IPV4_ANCHOR_0}

Install

[Install]通常是配置文件的最后一个区块,用来定义如何启动,以及是否开机启动,这个部分主要设置把该 unit 安装到哪个target。

  • WantedBy:它的值是一个或多个 Target,当前 Unit 激活时(enable)符号链接会放入/etc/systemd/system目录下面以 Target 名 + .wants后缀构成的子目录中(弱依赖),即是等待xxx的全部内容就绪就安装启动
  • RequiredBy:它的值是一个或多个 Target,当前 Unit 激活时,符号链接会放入/etc/systemd/system目录下面以 Target 名 + .required后缀构成的子目录中(强依赖)
  • Alias:当前 Unit 可用于启动的别名
  • Also:当前 Unit 激活(enable)时,会被同时激活的其他 Unit

systemd.service 中文手册

Service Unit实践

1. 在/etc/systemd/system/创建Unit脚本

查看root用户所在的组,以及组内成员 groups root

创建服务脚本:vi /etc/systemd/system/myService.service

注:这里不能用软连接。直接在此路径下创建service脚本。因为取消自启动命令会把软连接一并删除的。到时又要手动创建软连接才能用,麻烦。

jar服务输入下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码[Unit]
Description=My Spring Boot Service
After=syslog.target

[Service]
##类型为fork子线程执行,不然服务会重复重启到最后报错
Type=forking
#true:tmp共享可使用jps命令查看java进程;false:私有,只能用ps -ef|grep查看
#PrivateTmp=true
User=root
Group=root
##这里只配置开启命令就行。关闭和重启systemd会帮我控制,配置也是可以的。
ExecStart=/app/self-start/start.sh

### 可以使用方法1的上面myApp脚本,配置开启,重启,关闭执行的命令
#ExecStart=/app/self-start/myApp.sh start
#ExecReload=/app/self-start/myApp.sh restart
#ExecStop=/app/self-start/myApp.sh stop
Restart=always
#RestartPreventExitStatus=1

[Install]
WantedBy=multi-user.target

注意:

  • PrivateTmp=true
    • 服务启动时会在/tmp/目录下生成类似:/tmp/systemd-private-bceaa1ad35764ee2aea3cdd52fab89cd-myService.service-rVFD0q/tmp/hsperfdata_root的文件夹,用于存放myService的临时文件。存放了线程的pid-私有的,只能对自己账号有操作权限
    • jps命令使查询/tmp/目录下得共享文件,所有配置了PrivateTmp=true的进程用该命令使看不到的。请使用:ps -ef|grep java
  • PrivateTmp=false
    • 服务启动时会在/tmp/目录下生成共享文件,存放了线程的pid-所有用户共有读写权限。
  • multi-user.target:等待多用户系统的全部内容就绪才安装启动

2. 创建启动脚本

vi /app/self-start/start.sh

启动服务并把进程的pid保存一份到自定义的文件/app/self-start/myApp.pid

1
2
sh复制代码#!/bin/sh
nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/myApp.log & echo $! >/app/self-start/myApp.pid

3. 赋予脚本和程序权限

chmod +x /app/self-start/start.sh

chmod +x /app/self-start/myApp.jar

4. 重新加载所有修改过的unit文件

systemctl daemon-reload

5.查看配置

能查看到配置证明服务文件没有语法错误,逻辑和环境等错误查不到,要看启动日志

systemctl show -p ExecStart myService

6.启动服务并配置开机启动

启动:sudo systemctl start myService

状态:sudo systemctl status myService

停止:sudo systemctl stop myService

重启:sudo systemctl restart myService

设置开机启动:sudo systemctl enable myService

取消开机启动:sudo systemctl disable myService

查看服务启动日志(问题排查):journalctl -u myService.service

Timer Unit实践

Timer unit的详细配置

Timer 类型的 unit 主要用来执行定时任务,并有可能取代 cron 服务。

与服务类型的 unit 不同,timer unit 配置文件中的主要部分是 [Timer],下面是其主要的配置项:

[Timer]

  • OnActiveSec:定时器生效后,多少时间开始执行任务
  • OnBootSec:系统启动后,多少时间开始执行任务
  • OnStartupSec:Systemd 进程启动后,多少时间开始执行任务
  • OnUnitActiveSec:该单元上次执行后,等多少时间再次执行
  • OnUnitInactiveSec: 定时器上次关闭后多少时间,再次执行
  • OnCalendar:基于绝对时间,而不是相对时间执行
  • AccuracySec:如果因为各种原因,任务必须推迟执行,推迟的最大秒数,默认是60秒
  • Unit:真正要执行的任务,默认是同名的带有.service后缀的单元
  • Persistent:如果设置了该字段,即使定时器到时没有启动,也会自动执行相应的单元
  • WakeSystem:如果系统休眠,是否自动唤醒系统

1. 创建备份脚本

vi /app/self-start/backup.sh

1
2
3
4
5
6
7
8
sh复制代码#!/bin/bash
mydate()
{
date "+%Y%m%d%H%M%S"
}
backupdate=$(mydate)
## 打包需要备份的文件夹
tar -zcPf /app/self-start/bk.${backupdate}.tar.gz /app/self-start/bk

2. 赋予执行权限

sudo chmod +x /app/self-start/backup.sh

3. service unit 配置文件

vi /etc/systemd/system/backup.service

1
2
3
4
5
6
7
8
9
10
11
12
sh复制代码[Unit]
Description=my backup learn dir service

[Service]
User=root
Group=root
Type=simple
## 执行该脚本
ExecStart=/app/self-start/backup.sh

[Install]
WantedBy=multi-user.target

4. 配置定时服务

vi /etc/systemd/system/backup.timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sh复制代码[Unit]
Description=my backup learn dir timer

[Timer]
##每隔15分钟执行
#OnCalendar=*:0/15
#每月26号的凌晨0点半执行一次
#OnCalendar=*-*-26 00:30:00

#自定时器启动1分钟后间隔1秒执行一次
OnBootSec=1min
OnUnitActiveSec=30s

Persistent=true
## 配置要定时的服务
Unit=backup.service

[Install]
WantedBy=multi-user.target

5. 相关命令

刷新配置:sudo systemctl daemon-reload

开启备份服务: sudo systemctl start backup.service

开启定时服务:sudo systemctl start backup.timer

开机启动备份服务 : sudo systemctl enable backup.service

开机启动定时服务 : sudo systemctl enable backup.timer

查看状态备份服务: sudo systemctl status backup.service

查看状态定时服务: sudo systemctl status backup.timer

查看整个日志 :sudo journalctl

查看 backup.timer 的日志 :sudo journalctl -u backup.timer

查看 backup.timer 和 backup.service 的日志: sudo journalctl -u backup

从结尾开始查看最新日志:sudo journalctl -f

从结尾开始查看 backup.timer 的日志: journalctl -f -u backup.timer

方法3:直接开机启动配置

除了上面的注册服务并设置为开机自启动,还有一个方法就是直接配置开机自启动。

实践

1. 创建脚本

vi /app/self-start/directStart.sh

1
2
3
4
5
6
7
8
sh复制代码#!/bin/bash

##可以等待20秒再运行。等待其他服务或环境
##sleep 10
##这时一条test命令
echo "启动了" > /app/self-start/a.txt
##使用绝对路径的java命令。因为服务器重启相关环境可能还没准备好
nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &

2. 赋予脚本和程序权限

chmod +x /app/self-start/myApp.jar

chmod +x /app/self-start/directStart.sh

/etc/rc.local默认是没有执行权限的,要加上才能开机运行。

chmod +x /etc/rc.local

image.png

3. 修改开机启动文件/etc/rc.local

最好把命令绝对路径/bin/bash写全

1
sh复制代码/bin/bash /app/self-start/directStart.sh

4. 检查是否配置正常

手动运行测试是否可以。

bash /etc/rc.local

5. 重启测试

reboot

问题

  • 在linux下,如需添加随系统启动而自动运行的服务,只需在/etc/rc.local 脚本文件中添加即可。
  • 但是遇到一个问题是脚本手动测试能正常运行,放在/etc/rc.local中java命令没有正常运行或者执行失败。touch命令能成功。脚本中的命令不带路径的如下:
+ `nohup java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &`
  • 在系统重启执行这些命令时将报错,无法正常执行!究其原因:
+ `由于在执行rc.local脚本时,PATH环境变量未全部初始化,需在执行/etc/profile 后才被添加到环境变量PATH中。`
  • 为了在开机启动时执行,部分命令需要使用绝对路径
1
2
sh复制代码[root@k8s2 self-start]# which java
/usr/bin/java
  • nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &
  • 这和注册成服务相比是一个比较大的缺点,不能控制脚本启动时机的细腻度和需要什么样的环境。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker容器下快速搭建Hyperf框架在Windows系

发表于 2021-11-30

Hyperf框架介绍

Hyperf是一个高性能、高灵活性的渐进式 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于PHP-FPM的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于PSR标准实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是可替换与可复用的。

Docker下开发


假设您的本机环境并不能达到Hyperf的环境要求,或对于环境配置不是那么熟悉,那么您可以通过以下方法来运行及开发Hyperf项目,这里我们主要说明在Windows操作系统下的开发。

下载并安装docker


在Windows系统中进行开发,我们需要先安装docker for windows,首先直接进入docker官网:www.docker.com

然后依次点击:Get started -> Download for Windows,即可下载docker

下载包可能比较大,如果网络不太好的话需要耐心等待一下,待下载完毕后直接下一步安装

启动报WSL2问题


安装完毕后,第一次打开可能会报WSL2问题,直接按照提示里的URL地址打开,
,下载适用于x64计算机的WSL2 Linux内核更新包 ,下载完毕后也是直接安装然后重启docker客户端即可

拉取docker镜像(hyperf官方)

这里我们使用的命令行工具是powershell,同时按win + r,再输入powershell后回车,即可打开命令行工具

然后在命令行中输入:docker pull hyperf/hyperf:7.4-alpine-v3.11-swoole,下载完docker镜像后,我们可以使用docker image ls查看镜像列表,也可以在docker客户端的Images看到。

将本机项目目录绑定到镜像内的目录

假设在我本机电脑的目录D:\project\hyperf下是hyperf的项目,并且端口是默认的9501,现在我们将这个目录绑定到docker镜像中,即可实现在docker中开发,命令如下:

docker run -d --name hyperf -v D:\project\hyperf:/data/hyperf -p 9501:9501 -it --privileged -u root --entrypoint /bin/sh hyperf/hyperf:7.4-alpine-v3.11-swoole

由于我们前面已经拉取了镜像,所以这个命令会创建一个名为hyperf的容器,我们使用命令dcoker ps即可看到我们刚运行的容器的NAMES为hyperf,而且端口号是9501

进入容器


前面我们已经创建了一个名为还有hyperf的项目,所以我们需要执行命令docker exec -it hyperf bash即可进入项目镜像中

在容器内安装Composer

首先下载composer.phar,可以通过以下方式下载,composer不同版本的查看地址github.com/composer/co…:

wget https://github.com/composer/composer/releases/download/2.1.12/composer.phar

php -r "readfile('https://getcomposer.org/installer');" | php

下载完成后,再执行命令chmod u+x composer.phar给加上执行权限

再运行命令行mv composer.phar /usr/local/bin/composer,将composer.phar移动至/usr/local/bin目录下并改名为composer,即可实现全局调用了

然后运行命令composer,即可测试是否安装成功

设置Composer国内镜像

阿里云镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer

腾讯云镜像 composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer

国内Composer镜像 composer config -g repo.packagist composer https://packagist.phpcomposer.com

接触镜像绑定并恢复到官方镜像 composer config -g --unset repos.packagist

启动Hyperf服务


由于我们的项目是绑定在镜像目录是/data/hyperf,所以,我们再执行cd /data/hyperf即可进入项目目录中

然后再执行命令php bin/hyperf.php start,即可启动服务,如果有报错信息,根据错误信息依次解决然后再重启即可

启动服务后,可以同时按Ctrl+C,即可停止服务

测试服务


服务启动后,直接在浏览器地址栏中输入127.0.0.1:9501,然后能正常访问,说明服务已经启动成功了,到此说明开发环境基本搭建完成,可以愉快的Coding了……

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…109110111…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%