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

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


  • 首页

  • 归档

  • 搜索

ElasticSearch——ES在 Linux 环境下的单

发表于 2021-11-27

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

安装elasticsearch,必要条件就是先要保证安装了jdk。

1.1、ES下载

下载地址:www.elastic.co/cn/download…

在这里插入图片描述

下载后使用 xftp 后传输到Linux服务器上。

在这里插入图片描述

1.2、解压安装

输入命令解压:

1
2
bash复制代码#解压缩
tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz

在这里插入图片描述

elasticsearch-7.8.0太长,改名成es:

1
bash复制代码mv elasticsearch-7.8.0 es

1.3、创建用户

因为安全问题,Elasticsearch不允许 root用户直接运行,所以要创建新用户,在 root用户中创建新用户:

1
2
3
4
5
6
bash复制代码useradd es #新增用户es
passwd es #用户密码
#再输入两次密码(自定义),用户密码也为es
chown -R es:es /usr/local/es #将es文件使用权限赋予用户es
#补充:如果需要删除用户es
userdel -r es

1.4、修改配置文件

进入到es安装目录下的config文件夹中,修改elasticsearch.yml 文件:

1
bash复制代码cd /usr/local/es/config

在这里插入图片描述

使用vim编辑elasticsearch.yml 文件:

1
bash复制代码vim elasticsearch.yml

把下面配置添加到配置文件末尾:然后保存退出

1
2
3
4
5
6
7
8
9
10
bash复制代码#配置es的集群名称
cluster.name: elasticsearch
#当前节点名称
node.name: node-1
#设置当前的ip地址,通过指定相同网段的其他节点会加入该集群中
network.host: 0.0.0.0
#设置对外服务的http端口
http.port: 9200
#把当前节点当做集群的主节点
cluster.initial_master_nodes:["node-1"]

1.5、修改系统文件

在使用ElasticSearch时,会产生许多的文件,所以需要修改系统每个进程最大文件打开数:

1、修改/etc/security/limits.conf

1
bash复制代码vim /etc/security/limits.conf

文件末尾添加如下设置:然后保存退出

1
2
bash复制代码es soft nofile 65535
es hard nofile 65535

2、修改/etc/security/limits.d/20-nproc.conf

1
bash复制代码vim /etc/security/limits.d/20-nproc.conf

文件末尾添加如下设置:然后保存退出

1
2
bash复制代码es soft nofile 65535
es hard nofile 65535

3、修改 etc/sysctl.conf

1
bash复制代码vim etc/sysctl.conf

文件末尾添加如下设置:然后保存退出

1
bash复制代码vm.max_map_count=655360

重新加载

1
bash复制代码sysctl -p

1.6、启动ES软件

1、切换到es用户

1
bash复制代码su es

2、启动 ElasticSearch

在 ElasticSearch 文件目录下输入命令启动:

1
bash复制代码bin/elasticsearch

在这里插入图片描述

启动成功!

本文转载自: 掘金

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

图文并茂,Spring Boot Starter 万字详解!

发表于 2021-11-27

一、SpringBoot的starter简介

1.1 什么是starter(场景启动器)

在SpringBoot出现之前,如果我们想使用SpringMVC来构建我们的web项目,必须要做的几件事情如下:

  • 首先项目中需要引入SpringMVC的依赖
  • 在web.xml中注册SpringMVC的DispatcherServlet,并配置url映射
  • 编写springmcv-servlet.xml,在其中配置SpringMVC中几个重要的组件,处理映射器(HandlerMapping)、处理适配器(HandlerAdapter)、视图解析器(ViewResolver)
  • 在applicationcontext.xml文件中引入springmvc-servlet.xml文件
  • …

以上这几步只是配置好了SpringMVC,如果我们还需要与数据库进行交互,就要在application.xml中配置数据库连接池DataSource,如果需要数据库事务,还需要配置TransactionManager…

这就是使用Spring框架开发项目带来的一些的问题:

  • 依赖导入问题: 每个项目都需要来单独维护自己所依赖的jar包,在项目中使用到什么功能就需要引入什么样的依赖。手动导入依赖容易出错,且无法统一集中管理
  • 配置繁琐: 在引入依赖之后需要做繁杂的配置,并且这些配置是每个项目来说都是必要的,例如web.xml配置(Listener配置、Filter配置、Servlet配置)、log4j配置、数据库连接池配置等等。这些配置重复且繁杂,在不同的项目中需要进行多次重复开发,这在很大程度上降低了我们的开发效率

而在SpringBoot出现之后,它为我们提供了一个强大的功能来解决上述的两个痛点,这就是SpringBoot的starters(场景启动器)。

Spring Boot通过将我们常用的功能场景抽取出来,做成的一系列场景启动器,这些启动器帮我们导入了实现各个功能所需要依赖的全部组件,我们只需要在项目中引入这些starters,相关场景的所有依赖就会全部被导入进来,并且我们可以抛弃繁杂的配置,仅需要通过配置文件来进行少量的配置就可以使用相应的功能。

二、SpringBoot场景启动器的原理

在导入的starter之后,SpringBoot主要帮我们完成了两件事情:

  • 相关组件的自动导入
  • 相关组件的自动配置

这两件事情统一称为SpringBoot的自动配置

2.1 自动配置原理

2.1.1 自动配置类的获取与注入

我们从主程序入口来探索一下整个过程的原理:

1
2
3
4
5
6
7
typescript复制代码@SpringBootApplication //标注这个类是一个springboot的应用
public class CommunityApplication {
public static void main(String[] args) {
//将springboot应用启动
SpringApplication.run(CommunityApplication.class, args);
}
}

Spring Boot 基础就不介绍了,推荐下这个实战教程:
github.com/javastacks/…

@SpringBootApplication注解内部结构如下图所示:

AutoConfigurationImportSelector :重点看该类中重写的selectImports方法,看下它返回的字符串数组是如何得来的:

我们可以去到上边提到的spring.factories文件中去看一下,找到spring官方提供的spring-boot-autoconfigure包,在其下去找一下该文件:

可以看到这个就是SpringBoot官方为我们提供的所有自动配置类的候选列表。我们可以在其中找到一个我们比较熟悉的自动配置类去看一下它内部的实现:

可以看到这些一个个的都是JavaConfig配置类,而且都通过@Bean注解向容器中注入了一些Bean

结论:

  • SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的所有自动配置类的全限定类名
  • 将这些自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作;
  • 整个J2EE的整体解决方案和自动配置都在spring-boot-autoconfigure的jar包中;
  • 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件,并配置好这些组件 ;
  • 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作;

2.1.2 自动配置的过程

自动配置类被注入到容器当中后,会帮我们进行组件的自动配置和自动注入的工作,我们以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释这个过程:

首先我们先看下SpringBoot中配置文件与POJO类之间映射的方法,这是进行自动配置的基础。

配置集中化管理:SpringBoot中所有可配置项都集中在一个文件中(application.yml),这个文件中的配置通过@ConfigurationProperties注解来与我们程序内部定义的POJO类来产生关联,这些POJO类统一命名为xxxProperties,并且这些xxxProperties类中各个属性字段都有自己的默认值,这也是SpringBoot约定大于配置理念的体现,尽可能减少用户做选择的次数,但同时又不失灵活性。只要我们想,配置文件中的配置随时可以覆盖默认值。

之后,通过配合@EnableConfigurationProperties注解,就可以自动将与配置文件绑定好的这个类注入到容器中供我们使用。

自动配置类的工作流程:

  • 根据限定的条件向容器中注入组件
  • 使用xxxProperties对注入的组件的相关属性进行配置
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
less复制代码//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration

//将与配置文件绑定好的某个类注入到容器中,使其生效
//进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
//并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties(HttpProperties.class)

//Spring底层@Conditional注解
//根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
//这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)

//判断系统中有没有CharacterEncodingFilter这个类,如果有配置类才生效
@ConditionalOnClass(CharacterEncodingFilter.class)

//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
//matchIfMissing = true表明即使我们配置文件中不配置pring.http.encoding.enabled=true,该配置类也是默认生效的;
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

//该类已经与配置文件绑定了
private final HttpProperties.Encoding properties;

//构建该自动配置类时将与配置文件绑定的配置类作为入参传递进去
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name()); //注入bean时使用配置类中属性的值进行初始化,相当于将配置文件中的值映射到了组件的某些属性上
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter; //注入配置好的bean
}
}

一句话总结下自动配置类的工作过程 :

  • 首先容器会根据当前不同的条件判断,决定这个配置类是否生效!
  • 一但这个配置类生效;这个配置类就会给容器中添加相应组件;
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着,配置文件可以配置什么内容,可以参照该前缀对应的属性类中的属性字段
1
2
3
4
5
kotlin复制代码//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}

2.2 SpringBoot自动配置使用总结

  • SpringBoot启动会加载大量的自动配置类
  • 我们首先可以看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
  • 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
  • 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
    • xxxxAutoConfigurartion:自动配置类;给容器中添加组件
      • xxxxProperties:封装配置文件中相关属性;

了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;@Conditional派生注解(Spring注解版原生的@Conditional作用)

作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置里面的所有内容才生效;

那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

我们怎么知道哪些自动配置类生效?

我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

1
2
ini复制代码#在配置文件中开启springboot的调试类
debug=true

Positive matches:(自动配置类启用的:正匹配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码Positive matches:
-----------------

AopAutoConfiguration matched:
- @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice', 'org.aspectj.weaver.AnnotatedElement' (OnClassCondition)
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

AopAutoConfiguration.CglibAutoProxyConfiguration matched:
- @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)

AuditAutoConfiguration#auditListener matched:
- @ConditionalOnMissingBean (types: org.springframework.boot.actuate.audit.listener.AbstractAuditListener; SearchStrategy: all) did not find any beans (OnBeanCondition)

AuditAutoConfiguration#authenticationAuditListener matched:
- @ConditionalOnClass found required class 'org.springframework.security.authentication.event.AbstractAuthenticationEvent' (OnClassCondition)
- @ConditionalOnMissingBean (types: org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener; SearchStrategy: all) did not find any beans (OnBeanCondition)

Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码Negative matches:
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

AopAutoConfiguration.JdkDynamicAutoProxyConfiguration:
Did not match:
- @ConditionalOnProperty (spring.aop.proxy-target-class=false) did not find property 'proxy-target-class' (OnPropertyCondition)

AppOpticsMetricsExportAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'io.micrometer.appoptics.AppOpticsMeterRegistry' (OnClassCondition)

ArtemisAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

Exclusions、Unconditional classes(排除的、没有限定条件的自动配置类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码Exclusions:
-----------

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Unconditional classes:
----------------------

org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration

org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration

org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration

三、自定义场景启动器

现在我们已经了解了场景启动器的概念以及其隐藏在背后的自动配置的原理,我们就可以自己来对SpringBoot进行功能拓展,定义我们自己的场景启动器。

3.1 starter的命名规范

官方命名空间

  • 前缀:spring-boot-starter-
  • 模式:spring-boot-starter-模块名
  • 举例:spring-boot-starter-web、spring-boot-starter-jdbc

自定义命名空间

  • 后缀:-spring-boot-starter
  • 模式:模块-spring-boot-starter
  • 举例:mybatis-spring-boot-starter

推荐一个 Spring Boot 基础教程及实战示例:
github.com/javastacks/…

3.2 starter模块整体结构

通过上边的介绍,可以总结starter的整体实现逻辑主要由两个基本部分组成:

xxxAutoConfiguration:自动配置类,对某个场景下需要使用到的一些组件进行自动注入,并利用xxxProperties类来进行组件相关配置

xxxProperties:某个场景下所有可配置属性的集成,在配置文件中配置可以进行属性值的覆盖 按照SpringBoot官方的定义,Starer的作用就是依赖聚合,因此直接在starter内部去进行代码实现是不符合规定的,starter应该只起到依赖导入的作用,而具体的代码实现应该去交给其他模块来实现,然后在starter中去引用该模块即可,因此整体的starter的构成应该如下图所示:

可见starter模块依赖了两部分,一部分是一些常用依赖,另一部分就是对自动配置模块的依赖,而xxxAutoConfiguration与xxxProperties的具体实现,都封装在自动配置模块中,starter实际是通过该模块来对外提供相应的功能。

3.3 autoconfigure模块开发

3.3.1 依赖引入

首先所有的自动配置模块都要引入两个jar包依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<!-- 包含很多与自动配置相关的注解的定义,必须要引入 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<!-- 非必须的,引入后可以在配置文件中输入我们自定义配置的时候有相应的提示,也可以通过其他.properties文件为相关类进行属性映射(SpringBoot默认使用application.yml)-->
<optional>true</optional>
</dependency>
<dependencies>

其他依赖的选择根据项目需要进行添加即可

3.3.2 xxxAutoConfiguration的实现

autoconfigure模块中最重要的就是自动配置类的编写,它为我们实现组件的自动配置与自动注入。

在编写自动配置类的时候,我们应该要考虑向容器中注入什么组件,如何去配置它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Configuration
@ConditionalOnxxx
@ConditionalOnxxx//限定自动配置类生效的一些条件
@EnableConfigurationProperties(xxxProperties.class)
public class xxxAutoConfiguration {
@Autowired
private xxxProperties properties;
@Bean
public static BeanYouNeed beanYouNeed() {
BeanYouNeed bean = new BeanYouNeed()
bean.setField(properties.get(field));
bean.setField(properties.get(field));
bean.setField(properties.get(field));
......
}
}

3.3.3 xxxProperties的实现

这是跟配置文件相绑定的类,里边的属性就是我们可以在配置文件中配置的内容,然后通过@ConfigurationProperties将其与配置文件绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@ConfigurationProperties(prefix = "your properties") //使用@ConfigurationProperties注解绑定配置文件
public class xxxProperties {

private boolean enabled = true;

private String clientId;

private String beanName;

private String scanBasePackage;

private String path;

private String token;
}

3.3.4 配置spring.factories文件

在resource目录下新建META-INF文件夹,在文件夹下新建spring.factories文件,并添加写好的xxxAutoConfiguration类:

1
2
ini复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.meituan.xframe.boot.mcc.autoconfigure.xxxAutoConfiguration

3.4 Starter模块开发

starter模块中只进行依赖导入,在pom文件中添加对autoconfigure模块的依赖,并添加一些其他必要的依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<dependencies>
================================================================
<!--添加了对autoconfigure模块的引用-->
<dependency>
<groupId>com.test.starter</groupId>
<artifactId>xxx-spring-boot-autoconfigure</artifactId>
</dependency>
===============================================================
<!--其他的一些必要依赖项-->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
</dependencies>

这两个模块都开发完成之后,通过mvn install命令或者deploy命令将包发布到本地或者中央仓库,即可直接在其他项目中引用我们自定义的starter模块了

来源:blog.csdn.net/qq_21310939/article/details/107401400

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.6 正式发布,一大波新特性。。

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

本文转载自: 掘金

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

数据库时间慢了14个小时,Mybatis说,这个锅我不背!

发表于 2021-11-27

同事反馈一个问题:Mybatis插入数据库的时间是昨天的,是不是因为生成Mybatis逆向工程生成的代码有问题?

大家都知道,对于这类Bug本人是很感兴趣的。直觉告诉我,应该不是Mybatis的Bug,很可能是时区的问题。

很好,今天又可以带大家一起来排查Bug了,看看从这次的Bug排查中你能Get什么技能。

这次研究的问题有点深奥,但结论很重要。Let’s go!

问题猜想

同事反馈问题的时候,带了自己的猜想:是不是数据库字段设置为datetime导致?是不是Mybatis逆向工程生成的代码中类型不一致导致的?

同事还要把datetime改为varchar……马上被我制止了,说:先排查问题,再说解决方案,下午我也抽时间看看。

问题核查

第一步,检查数据库字段类型,是datetime的,没问题。

第二步,检查实体类中类型,是java.util.Date类型,没问题。

第三步,Bug复现。

在Bug复现这一步,用到了单元测试。话说之前还跟朋友讨论过单元测试的魅力,现在本人是越来越喜欢单元测试了。

项目基于Spring Boot的,单元测试如下(代码已脱敏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@SpringBootTest
class DateTimeTests {
​
@Resource
private UserMapper userMapper;
​
@Test
public void testDate(){
User user = new User();
// 省略其他字段
user.setCreateDate(new Date());
userMapper.insertSelective(user);
}
}

执行单元测试,查看数据库中插入的数据。Bug复现,时间的确是前一天的,与当前时间相差14个小时。

经过上面三步的排查,核实了数据库字段和代码中类型没问题。单元测试也复现了问题,同事没有欺骗我,总要眼见为实,哈哈~

于是基本确定是时区问题。

时区排查

检查服务器时间

登录测试服务器,执行date命令,检查服务器时间和时区:

1
2
3
4
csharp复制代码[root@xxx ~]# date
2021年 11月 25日 星期四 09:26:25 CST
[root@xxx ~]# date -R
Thu, 25 Nov 2021 09:33:34 +0800

显示时间是当前时间,采用CST时间,最后的+0800,即东8区,没问题。

检查数据库时区

连接数据库,执行show命令:

1
2
3
4
5
6
7
sql复制代码show variables like '%time_zone%';
​
+----------------------------+
|Variable         | Value |
+----------------------------+
|system_time_zone |CST   |
|time_zone     |SYSTEM |

system_time_zone:全局参数,系统时区,在MySQL启动时会检查当前系统的时区并根据系统时区设置全局参数system_time_zone的值。值为CST,与系统时间的时区一致。

time_zone:全局参数,设置每个连接会话的时区,默认为SYSTEM,使用全局参数system_time_zone的值。

检查代码中时区

在单元测试的方法内再添加打印时区的代码:

1
2
3
4
5
6
7
8
typescript复制代码@Test
public void testDate(){
System.out.println(System.getProperty("user.timezone"));
User user = new User();
// 省略其他字段
user.setCreateDate(new Date());
userMapper.insertSelective(user);
}

打印的时区为:

1
复制代码Asia/Shanghai

也就是说Java中使用的是UTC时区进行业务逻辑处理的,也是东八区的时间。

那么问题到底出在哪里呢?

问题基本呈现

经过上述排查,基本上确定是时区的问题。这里,再补充一下上述相关的时区知识点。

UTC时间

UTC时间:世界协调时间(UTC)是世界上不同国家用来调节时钟和时间的主要时间标准,也就是零时区的时间。

UTC, Coordinated Universal Time是一个标准,而不是一个时区。UTC 是一个全球通用的时间标准。全球各地都同意将各自的时间进行同步协调 (coordinated),这也是UTC名字的来源:Universal Coordinated Time。

CST时间

CST时间:中央标准时间。

CST可以代表如下4个不同的时区:

  • Central Standard Time (USA) UT-6:00,美国
  • Central Standard Time (Australia) UT+9:30,澳大利亚
  • China Standard Time UT+8:00,中国
  • Cuba Standard Time UT-4:00,古巴

再次分析

很显然,这里与UTC时间无关,它只是时间标准。目前Mysql中的system_time_zone是CST,而CST可以代表4个不同的时区,那么,Mysql把它当做哪个时区进行处理了呢?

简单推算一下,中国时间是UT+8:00,美国是 UT-6:00,当传入中国时间,直接转换为美国时间(未考虑时区问题),时间便慢了14个小时。

既然知道了问题,那么解决方案也就有了。

解决方案

针对上述问题可通过数据库层面和代码层面进行解决。

方案一:修改数据库时区

既然是Mysql理解错了CST指定的时区,那么就将其设置为正确的。

连接Mysql数据库,设置正确的时区:

1
2
3
4
ini复制代码[root@xxxxx ~]# mysql -uroot -p
mysql> set global time_zone = '+8:00';
mysql> set time_zone = '+8:00'
mysql> flush privileges;

再次执行show命令:

1
2
3
4
5
6
7
sql复制代码show variables like '%time_zone%';
​
+----------------------------+
|Variable         | Value |
+----------------------------+
|system_time_zone |CST   |
|time_zone     |+08:00 |

可以看到时区已经成为东八区的时间了。再次执行单元测试,问题得到解决。

此种方案也可以直接修改MySQL的my.cnf文件进行指定时区。

方案二:修改数据库连接参数

在代码连接数据库时,通过参数指定所使用的时区。

在配置数据库连接的URL后面添加上指定的时区serverTimezone=Asia/Shanghai:

1
bash复制代码url: jdbc:mysql://xx.xx.xx.xx:3306/db_name?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai

再次执行单元测试,问题同样可以得到解决。

问题完了?

经过上述分析与操作,时区的问题已经解决了。问题就这么完事了吗?为什么是这样呢?

为了验证时区问题,在时区错误的数据库中,创建了一个字段,该字段类型为datetime,默认值为CURRENT_TIMESTAMP。

那么,此时插入一条记录,让Mysql自动生成该字段的时间,你猜该字段的时间是什么?中国时间。

神奇不?为什么同样是CST时区,系统自动生成的时间是正确的,而代码插入的时间就有时差问题呢?

到底是Mysql将CST时区理解为美国时间了,还是Mybatis、连接池或驱动程序将其理解为美国时间了?

重头戏开始

为了追查到底是代码中哪里出了问题,先开启Mybatis的debug日志,看看insert时是什么值:

1
vbnet复制代码2021-11-25 11:05:28.367 [|1637809527983|] DEBUG 20178 --- [   scheduling-1] c.h.s.m.H.listByCondition                : ==> Parameters: 2021-11-25 11:05:27(String), 0(Integer), 1(Integer), 2(Integer), 3(Integer), 4(Integer)

上面是insert时的参数,也就是说在Mybatis层面时间是没问题的。排除一个。

那是不是连接池或驱动程序的问题?连接池本身来讲跟数据库连接的具体操作关系不大,就直接来排查驱动程序。

Mybatis是xml中定义日期字段类型为TIMESTAMP,扒了一下mysql-connector-Java-8.0.x的源码,发现SqlTimestampValueFactory是用来处理TIMESTAMP类型的。

在SqlTimestampValueFactory的构造方法上打上断点,执行单元测试:

timezone

可以明确的看到,Calendar将时区设置为Locale.US,也就是美国时间,时区为CST,offset为-21600000。-21600000单位为毫秒,转化为小时,恰好是“-6:00”,这与北京时间“GMT+08:00”恰好相差14个小时。

于是一路往上最终追溯调用链路,该TimeZone来自NativeServerSession的serverTimeZone,而serverTimeZone的值是由NativeProtocol类的configureTimezone方法设置的。

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
csharp复制代码public void configureTimezone() {
      String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
​
      if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
          configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
      }
​
      String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();
​
      if (configuredTimeZoneOnServer != null) {
          // user can override this with driver properties, so don't detect if that's the case
          if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
              try {
                  canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
              } catch (IllegalArgumentException iae) {
                  throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
              }
          }
      }
​
      if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
      // 此处设置TimeZone
          this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));
​
          if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {
              throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
                      getExceptionInterceptor());
          }
      }
​
  }

debug跟踪一下上述代码,显示信息如下:

CST获得

至此,通过canonicalTimezone值的获取,可以看出URL后面配置serverTimezone=Asia/Shanghai的作用了。其中,上面第一个代码块获取time_zone的值,第二个代码块中获取system_time_zone的值。这与查询数据库获得的值一致。

因为出问题时并未在url中添加参数serverTimezone=Asia/Shanghai,所以走canonicalTimezone为null的情况。随后逻辑中调用了TimeUtil.getCanonicalTimezone方法:

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
typescript复制代码public static String getCanonicalTimezone(String timezoneStr, ExceptionInterceptor exceptionInterceptor) {
      if (timezoneStr == null) {
          return null;
      }
​
      timezoneStr = timezoneStr.trim();
​
      // handle '+/-hh:mm' form ...
      if (timezoneStr.length() > 2) {
          if ((timezoneStr.charAt(0) == '+' || timezoneStr.charAt(0) == '-') && Character.isDigit(timezoneStr.charAt(1))) {
              return "GMT" + timezoneStr;
          }
      }
​
      synchronized (TimeUtil.class) {
          if (timeZoneMappings == null) {
              loadTimeZoneMappings(exceptionInterceptor);
          }
      }
​
      String canonicalTz;
      if ((canonicalTz = timeZoneMappings.getProperty(timezoneStr)) != null) {
          return canonicalTz;
      }
​
      throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,
              Messages.getString("TimeUtil.UnrecognizedTimezoneId", new Object[] { timezoneStr }), exceptionInterceptor);
  }

上述代码中最终走到了loadTimeZoneMappings(exceptionInterceptor);方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码private static void loadTimeZoneMappings(ExceptionInterceptor exceptionInterceptor) {
      timeZoneMappings = new Properties();
      try {
          timeZoneMappings.load(TimeUtil.class.getResourceAsStream(TIME_ZONE_MAPPINGS_RESOURCE));
      } catch (IOException e) {
          throw ExceptionFactory.createException(Messages.getString("TimeUtil.LoadTimeZoneMappingError"), exceptionInterceptor);
      }
      // bridge all Time Zone ids known by Java
      for (String tz : TimeZone.getAvailableIDs()) {
          if (!timeZoneMappings.containsKey(tz)) {
              timeZoneMappings.put(tz, tz);
          }
      }
  }

该方法加载了配置文件”/com/mysql/cj/util/TimeZoneMapping.properties”里面的值,经过转换,timeZoneMappings中,对应CST的为”CST”。

最终得到canonicalTimezone为“CST”,而TimeZone获得是通过TimeZone.getTimeZone(canonicalTimezone)方法获得的。

也就是说TimeZone.getTimeZone(“CST”)的值为美国时间。写个单元测试验证一下:

1
2
3
4
5
6
7
csharp复制代码public class TimeZoneTest {
​
@Test
public void testTimeZone(){
System.out.println(TimeZone.getTimeZone("CST"));
}
}

打印结果:

1
erlang复制代码sun.util.calendar.ZoneInfo[id="CST",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=CST,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]

很显然,该方法传入CST之后,默认是美国时间。

至此,问题原因基本明朗:

  • Mysql中设置的server_time_zone为CST,time_zone为SYSTEM。
  • Mysql驱动查询到time_zone为SYSTEM,于是使用server_time_zone的值,为”CST“ 。
  • JDK中TimeZone.getTimeZone(“CST”)获得的值为美国时区;
  • 以美国时区构造的Calendar类;
  • SqlTimestampValueFactory使用上述Calendar来格式化系统获取的中国时间,时差问题便出现了;
  • 最终反映在数据库数据上就是错误的时间。

serverVariables变量

再延伸一下,其中server_time_zone和time_zone都来自于NativeServerSession的serverVariables变量,该变量在NativeSession的loadServerVariables方法中进行初始化,关键代码:

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
erlang复制代码if (versionMeetsMinimum(5, 1, 0)) {
              StringBuilder queryBuf = new StringBuilder(versionComment).append("SELECT");
              queryBuf.append(" @@session.auto_increment_increment AS auto_increment_increment");
              queryBuf.append(", @@character_set_client AS character_set_client");
              queryBuf.append(", @@character_set_connection AS character_set_connection");
              queryBuf.append(", @@character_set_results AS character_set_results");
              queryBuf.append(", @@character_set_server AS character_set_server");
              queryBuf.append(", @@collation_server AS collation_server");
              queryBuf.append(", @@collation_connection AS collation_connection");
              queryBuf.append(", @@init_connect AS init_connect");
              queryBuf.append(", @@interactive_timeout AS interactive_timeout");
              if (!versionMeetsMinimum(5, 5, 0)) {
                  queryBuf.append(", @@language AS language");
              }
              queryBuf.append(", @@license AS license");
              queryBuf.append(", @@lower_case_table_names AS lower_case_table_names");
              queryBuf.append(", @@max_allowed_packet AS max_allowed_packet");
              queryBuf.append(", @@net_write_timeout AS net_write_timeout");
              queryBuf.append(", @@performance_schema AS performance_schema");
              if (!versionMeetsMinimum(8, 0, 3)) {
                  queryBuf.append(", @@query_cache_size AS query_cache_size");
                  queryBuf.append(", @@query_cache_type AS query_cache_type");
              }
              queryBuf.append(", @@sql_mode AS sql_mode");
              queryBuf.append(", @@system_time_zone AS system_time_zone");
              queryBuf.append(", @@time_zone AS time_zone");
              if (versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))) {
                  queryBuf.append(", @@transaction_isolation AS transaction_isolation");
              } else {
                  queryBuf.append(", @@tx_isolation AS transaction_isolation");
              }
              queryBuf.append(", @@wait_timeout AS wait_timeout");
​
              NativePacketPayload resultPacket = sendCommand(this.commandBuilder.buildComQuery(null, queryBuf.toString()), false, 0);
              Resultset rs = ((NativeProtocol) this.protocol).readAllResults(-1, false, resultPacket, false, null,
                      new ResultsetFactory(Type.FORWARD_ONLY, null));
              Field[] f = rs.getColumnDefinition().getFields();
              if (f.length > 0) {
                  ValueFactory<String> vf = new StringValueFactory(this.propertySet);
                  Row r;
                  if ((r = rs.getRows().next()) != null) {
                      for (int i = 0; i < f.length; i++) {
                          this.protocol.getServerSession().getServerVariables().put(f[i].getColumnLabel(), r.getValue(i, vf));
                      }
                  }
              }
​

在上述StringBuilder的append操作中,有”@@time_zone AS time_zone”和”@@system_time_zone AS system_time_zone”两个值,然后查询数据库,从数据库获得值之后,put到serverVariables中。

再来debug一下:

system_time_zone

可以看出system_time_zone的值为CST。

time_zone

同样time_zone的值为“SYSTEM”。

根据代码中的提示,拼接与代码一样的SQL查询一下数据库:

1
2
sql复制代码select @@time_zone;
SYSTEM

值的确是“SYSTEM”。此时,我们又得出另外一个查询Mysql当前时区的方法。

至此,该问题的排查完美收官。大出一口气~

小结

在上述问题排查的过程中,多次用到单元测试,这也是单元测试的魅力所在,用最简单的代码,最轻量的逻辑,最节省时间的方式来验证和追踪错误。

再回顾一下上述Bug排查中用到和学到的知识点:

  • Linux日期查看,时区查看及衍生如何配置时区;
  • Mysql时区查看;
  • Spring Boot单元测试;
  • Java时区获取;
  • UTC时间和CST时间;
  • 两种解决时区问题的方案;
  • 阅读、debug Mysql驱动源代码;
  • TimeZone.getTimeZone(“CST”)默认时区为美国时区;
  • Mysql驱动中处理时区问题基本流程逻辑;
  • Mybatis debug日志相关打印;
  • 其他相关知识。

通过本篇Bug查找的文章,你学到了什么?如果有那么一点启发,不要吝啬,给点个赞吧!

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

大话数据结构--图的存储结构 72图的抽象数据类型 73

发表于 2021-11-27

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

7.2图的抽象数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
markdown复制代码 ADT图(Graph)
 Data
  顶点的有穷非空集合和边的集合。
 Operation
     CreateGraph (*G,V,VR) :按照顶点集V和边弧集VR的定义构造图G。
     DestroyGraph(*G) :图G存在则销毁。
     LocateVex(G,u) :若图G中存在顶点u,则返回图中的位置。
     GetVex (G,v) :返回图G中顶点v的值。
     PutVex (G,v,value) :将图G中顶点v赋值value。
     FirstAdjVex (G,*v) :返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空。
     NextAdjVex (G,v,*w) :返回顶点v相对于顶点w的下一一个邻接顶点,若w是v的最后
    一个邻接点则返回“空"。
     InsertVex (*G,v) :在图G中增添新顶点V。
     DeleteVex ( *G,v):删除图G中顶点v及其相关的弧。
     InsertArc ( *G,v,w):在图G中增添弧<v,w>,若G是无向图,还需要增添对称弧
    <w,v>。
     DeleteArc (*G,v,w):在图G中删除弧<v,w>,若G是无向图,则还删除对称弧
     DFSTraverse(G):对围G中进行深度优先遍历,在遍历过程对每个顶点调用。
     HFSTraverse(G) :对图G中进行广度优先遍历,在遍历过程对每个顶点调用。
 endADT
 ​

7.3图的存储结构

7.3.1领接矩阵

图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

1.无向图的邻接矩阵

image-20211117162429916

2.有向图的邻接矩阵

image-20211117173959652

分析1: 有向图的邻接矩阵可能是不对称的。 分析2: 顶点的出度=第i行元素之和 顶点的入度=第列元素之和 顶点的度=第i行元素之和+第j元素之和

3.网(即有权图)的邻接矩阵表示法

image-20211117174436567

代码实现:

用两个数组分别存储顶点表和邻接矩阵

1
2
3
4
5
6
7
8
9
10
arduino复制代码 #define MaxInt 32767    //表示极大值,即∞
 #define MVNum 100 //最大顶点数
 typedef char VerTexType; //设顶点的数据类型为字符型
 typedef int ArcType;//假设边的权值类型为整型
 ​
 typedef struct{
     VerTexType vexs{MVNum]; //顶点表
     ArcType arcs[MVNum][MVNum]; //邻接矩阵
     int vexnum, arcnum; //图的当前点数和边数。
 }AMGraph; // Adjacency Matrix Graph

本文转载自: 掘金

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

Java中的变量和数据类型

发表于 2021-11-27

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

变量的概述

变量是内存中存储数据最基本的单元,将数据放到内存当中,给这块内存空间起个名字,这就是变量。

所以变量就是内存当中的一块空间,这块空间有名字、有类型、有值,这也是变量必须具备的三要素。

变量要求“变量的数据类型”和变量中存储的“数据”必须是一致的,换句话说,冰箱是用来存放小食品的,所以大象不能往里面放,原因是放不下,空间不合适。例如:int 类型就只能存放4个字节大小的整数,再长一点儿就放不下,比如long 类型的整数占有8个字节,它是肯定无法放到int 类型的变量当中的。

所谓变量:可变化的量。它的意思是变量中存储的数据不是一成不变的,是可以被改变的,假设变量i 之前存放的数据是1,我们可以将其变成10,变量就是这个意思。总的来说,一个变量是有三要素组成的,分别是:数据类型、变量名、存储的值。

变量的使用

我们在使用变量之前需要先进行变量的声明。下面这段代码表示声明一个int 类型的变量age 用来存储年龄数据:

1
2
3
4
5
6
csharp复制代码public class VarTest01{
public static void main(Sting[] args){
int age;//年龄
System.out.println(age);
}
}

我们可以看到上面的代码中这个age 变量的三要素当中只具备了两个要素:数据类型和变量名,此时的age 变量并没有存储数据(没有赋值),显然这个age 变量不能被访问。也就是说age 变量中还没有数据,空间还没有开辟出来,由此可见,java 语言中的变量要求必须先声明,在赋值才能被访问。

变量使用的几个注意点

  • 1,“=”叫做赋值运算符,赋值运算符右边的表达式优先级较高,所以等号右边先执行,将执行结果赋给左边的变量。
  • 2,在进行赋值运算的时候,Java 中规定“值”的数据类型必须和“变量”的数据类型保持一致,也就是说int 类型的变量只能存储int 类型的数据,不能存储其他类型的数据。
  • 3,允许一次声明多个同类型的变量
1
ini复制代码int a=200,b=200,c=400;
  • 4,同一个域中变量名不能重名,但变量是可以重新赋值的。

变量的分类

变量根据声明位置的不同可以分为:局部变量和成员变量

数据类型概述

在学习数据类型之前我们先来思考一个问题,数据类型在程序中起什么作用呢?

实际上软件主要是对数据进行处理,现实生活中的数据有很多,所以编程语言对其进行了分类,然后就产生了数据类型,不同数据类型的数据会给其分配不同大小的空间进行存储。也就是说,数据类型作用就是决定程序运行阶段给变量分配多大的内存空间。

java 中的数据类型包括两大类,一类是基本数据类型,另一类是引用数据类型。其中,基本数据类型又包括以下 4 类 8 种:

  • 第1类:整数型(不带小数的数字):byte, short, int, long
  • 第2类:浮点型(带小数的数字):float,double
  • 第3类:字符型(文字,单个字符):char
  • 第4类:布尔型(真和假):boolean

我们也可以注意到,在以上基本数据类型中并没有字符串类型(带双引号的是字符串),这是因为,Java 中字符串属于引用数据类型,不属于基本数据类型的范畴。接下来我们看一看八种基本数据类型的详细信息:

数据类型 占字节数 缺省默认值
byte 1 0
short 2 0
int 4 0
long 8 0L
float 4 0.0f
double 8 0.0
boolean 1 false
char 2 ‘\u0000’

通过上表我们也可以注意到不同数据类型所占的字节数并不相同,那字节又是什么呢?1个字节就是8个比特位。那 1 个比特位又是什么呢?1 个比特位就是一个1或者0,或者说一个比特位就是一个二进制。也就是说1个字节是由8个1和0组成的二进制数字串。

有了这些基本的认知,接下来我们看看数据类型的详解!

数据类型详解

1,字符型详解

字符型char 在Java 语言中占用2个字节,char 类型的字面量必须用半角的单引号括起来,同时也要了解char 和short 虽都占用2个字节,但是char 可以取到更大的正整数,因为char 类型没有负数。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class CharTest{
public static void main(String[] args){
char c1='中';//char类型变量可以容纳一个汉字
char c2='t';
System.out.println("这是个普通的字符="+c2);
char c3='\t';
System.out.println("abc"+c3+"def");
}
}
//程序运行结果为
这是个普通的字符=t
abc def

对于以上的程序,’\t’表面上看是由两个字符构成,但程序能够正常运行的原因是****具有转义功能。下面看几个常见的转义字符的含义:

转义字符 意义 ASCII码值(十进制)
\n 换行,将当前位置移到下一行开头 010
\r 回车,将当前位置移到本行开头 013
\t 水平制表符(跳到下一个TAB位置 009
\ 代表一个反斜线字符’’ 092
‘ 代表一个单引号字符 039
“ 代表一个双引号字符 034

2,整数型详解

整数类型在Java中有4种表示方式,分别是十进制、八进制、十六进制、二进制。默认为十进制,以0开始
表示八进制,以 0x 开始表示十六进制,以 0b 开始表示二进制。

通过一段代码进一步理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public class InterTypeTest{
public static void main(String[] args){
int a=10;
long b=10L;
long c=10;
System.out.println(c);
int d=(int)c;//强制类型转化
System.out.println(d);
}
}
//代码运行结果为
10
10

通过上面的测试,我们可以得出如下一个数据赋值给一个变量的时候存在三种不同的情况:

  • 1,类型一致,不存在类型转换
  • 2,小容量可以自动赋值给大容量,称为自动类型转换
  • 3,大容量不能直接赋值给小容量,如果一定要赋值给小容量的话,必须添加强制类型转换符进行强制类型转换操作。

3,布尔型详解

在Java语言中布尔类型的值只包括 true 和 flase,没有其他值,不包括 0 和 1。布尔类型的数据在开发中主要用在逻辑判断方面。

接下来可以自行运行下面的一段代码,来看看布尔类型变量的值是否可以使用1 和 0:

1
2
3
4
5
6
typescript复制代码public class InterTypeTest{
public static void main(String[] args){
boolean flag=1;
boolean success=0;
}
}

通过这个测试,我们就可以切身体会到,在Java中布尔类型的变量值不能使用1 和 0,只能使用 true 和 false。

4,浮点型详解

浮点型数据实际上在内存中存储的时候大部分情况下都是存储了数据的近似值,为什么呢?因为数据是无穷的,但内存是有限的,所以只能存储近似值,float单精度占4个字节,double双精度占8个字节,相对来说double 精度高一些。

同时Java 语言中有这样一条规定:只要是浮点型的字面量,例如 1.0/3.14 等都会默认当做double类型来处理,如果想要程序将其当做 float 类型处理,需要在字面量后面添加 f/F 。

5,基本数据类型转换

基本数据类型之间是存在固定的转换规则的,下面总结了6条基本规则,可在平时的应用中进行套用:

  • 1, 8种数据类型除了boolean 类型不能转换,剩下的都可以进行转换;
  • 2,如果整数型字面量没有超出 byte,short,char,的取值范围,可以将其赋值给byte,short,char类型的变量;
  • 3,小容量可以向大容量自动转换。容量从小到大排序为:byte<short(char)<int <long<float<double,其中short 和 char都占用两个字节,但是char 可以表示更大的正整数;
  • 4,大容量转换成小容量,称为强制类型转换,编写是必须添加“强制类型转换符”,但是运行时可能出现精度损失,谨慎使用;
  • 5,byte,short,char类型混合运算时,先各自转换成int 类型在做运算;
  • 6,多种数据类型混合运算时,各自先转换成容量最大的那一种再做运算。

本文转载自: 掘金

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

Linux下目录编程(读取、创建、拷贝)

发表于 2021-11-27

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

一、前言

之前有几篇文章介绍了Linux下文件编程,那么目录和文件编程类似,也有一套函数,可以打开,读取、创建目录等。创建目录、文件除了命令以外(mkdir、touch),都有对应的函数实现相同功能。
使用较多的就是遍历目录的功能,比如: 音乐播放器需要循环播放指定目录下所有音频文件,视频播放器需要遍历指定目录查找所有的视频文件加入到播放列表等等。

目录操作相关函数如下:

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
cpp复制代码#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
函数功能: 打开目录
函数形参:
const char *name :打开的目录路径
返回值: 目录打开成功返回指向该目录的指针.

struct dirent *readdir(DIR *dirp);
函数功能: 读目录. 每调用一次就获取一次当前目录下一个文件的信息.
函数形参:
DIR *dirp :打开的目录指针.
返回值:保存当前读取成功的文件信息.
该函数可以重复调用,调用成功就返回当前目录下一个文件的信息,如果读取失败或者文件读取完毕返回NULL。

struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported
by all file system types */
char d_name[256]; /* filename */
};

int closedir(DIR *dirp);
函数功能: 关闭已经打开的目录.

#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
函数功能: 创建一个新目录.

二、案例代码

2.1 遍历指定目录: 实现ls -a命令功能

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
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>

int main(int argc,char **argv)
{
if(argc!=2)
{
printf("参数: ./a.out <目录的路径>\n");
return 0;
}
/*1. 打开目录*/
DIR *dir=opendir(argv[1]);
if(dir==NULL)
{
printf("%s 目录打开失败.\n",argv[1]);
return -1;
}
/*2. 遍历目录*/
struct dirent *dir_info;
while(dir_info=readdir(dir))
{
printf("%s ",dir_info->d_name);
}
printf("\n");
/*3. 关闭目录*/
closedir(dir);
return 0;
}

2.2 创建目录

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
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>

int main(int argc,char **argv)
{
if(argc!=2)
{
printf("参数: ./a.out <创建的新目录名称>\n");
return 0;
}
printf("即将创建的新目录名称:%s\n",argv[1]);

/*1. 调用mkdir函数创建目录*/
// printf("mkdir函数状态:%d\n",mkdir(argv[1],S_IWUSR|S_IRUSR));
// 成功返回为0 失败返回-1 ,该函数不能创建多层目录

/*2. 使用system函数调用系统命令完成目录的创建*/
char cmd_buf[100];
sprintf(cmd_buf,"mkdir %s -p",argv[1]);
system(cmd_buf);
return 0;
}

2.3 得到文件和目录的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <libgen.h>

int main(int argc,char **argv)
{
if(argc!=2)
{
printf("参数: ./a.out <路径>\n");
return 0;
}
//printf("目录名称:%s\n",dirname(argv[1]));
//传入: /123/456/789/a.c 返回/123/456/789

printf("文件名称:%s\n",basename(argv[1]));
//传入: /123/456/789/a.c 返回a.c
return 0;
}

2.4 命令行*.c传参的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码#include <stdio.h>
int main(int argc,char **argv)
{
int i;
for(i=0;i<argc;i++)
printf("%s\n",argv[i]);
return 0;
}

[wbyq@wbyq linux_c]$ ./a.out *.c
./a.out
123.c
456.c
app.c
[wbyq@wbyq linux_c]$ ./a.out \*.c
./a.out
*.c
[wbyq@wbyq linux_c]$

2.5 使用目录操作函数实现ls *.c

使用目录操作函数实现ls *.c 或者ls *.mp3 类似的功能.

*号是特殊符号. 是通配符符号。 ./a.out \*.c

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
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>

int main(int argc,char **argv)
{
if(argc!=3)
{
printf("参数: ./a.out <目录的路径> <后缀例如:.c .mp3>\n");
return 0;
}
/*1. 打开目录*/
DIR *dir=opendir(argv[1]);
if(dir==NULL)
{
printf("%s 目录打开失败.\n",argv[1]);
return -1;
}
/*2. 遍历目录*/
struct dirent *dir_info;
struct stat s_buf; //存放状态信息的
char *abs_path=NULL;
while(dir_info=readdir(dir))
{
if(strstr(dir_info->d_name,argv[2]))
{
//1. 申请空间
abs_path=malloc(strlen(argv[1])+strlen(dir_info->d_name)+1);
//2. 拼接路径
sprintf(abs_path,"%s%s",argv[1],dir_info->d_name);
printf("%s\n",abs_path);
//3. 释放空间
free(abs_path);
}
}
/*3. 关闭目录*/
closedir(dir);
return 0;
}

2.6 拷贝单层目录

实现cp命令的功能. 支持拷贝单层目录.

例如: cp 123.c 456.c 或者 cp abc/ work/ -a

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
int cp_file(const char *src_file,const char *new_file);

int main(int argc,char **argv)
{
if(argc!=3)
{
printf("参数: ./a.out <源目录的路径> <目标目录的路径>\n");
return 0;
}
/*1. 打开目录*/
DIR *dir=opendir(argv[1]);
if(dir==NULL)
{
printf("%s 目录打开失败.\n",argv[1]);
return -1;
}
/*2. 遍历目录*/
struct dirent *dir_info;
struct stat s_buf; //存放状态信息的
char *abs_path=NULL;
char *new_abs_path=NULL;
while(dir_info=readdir(dir))
{
//1. 申请空间
abs_path=malloc(strlen(argv[1])+strlen(dir_info->d_name)+1);
//2. 拼接路径
sprintf(abs_path,"%s%s",argv[1],dir_info->d_name);
//3.判断文件的类型
stat(abs_path,&s_buf);
if(S_ISREG(s_buf.st_mode)) //普通文件
{
new_abs_path=malloc(strlen(argv[2])+strlen(dir_info->d_name)+1);
sprintf(new_abs_path,"%s%s",argv[2],dir_info->d_name);
cp_file(abs_path,new_abs_path);
free(new_abs_path);
}
//4. 释放空间
free(abs_path);
}
/*3. 关闭目录*/
closedir(dir);
return 0;
}

/*
函数功能: 拷贝指定的文件
*/
int cp_file(const char *src_file,const char *new_file)
{
/*1. 打开源文件*/
FILE *src_fp=fopen(src_file,"rb");
if(src_fp==NULL)return -1;
/*2. 创建新文件*/
FILE *new_fp=fopen(new_file,"wb");
if(new_fp==NULL)return -2;
/*3. 拷贝文件*/
unsigned char buff[1024];
int cnt;
while(1)
{
cnt=fread(buff,1,1024,src_fp);
fwrite(buff,1,cnt,new_fp);
if(cnt!=1024)break;
}
/*4. 关闭文件*/
fclose(new_fp);
fclose(src_fp);
return 0;
}

本文转载自: 掘金

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

swagger系列教程二

发表于 2021-11-27

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

项目源码地址:github.com/Dr-Water/fa…

springfox默认会把所有api分成一组,这样通过类似于 http://127.0.0.1:8080/jadDemo/swagger-ui.html 这样的地址访问时,会在同一个页面里加载所有api列表。这样,如果系统稍大一点,api稍微多一点,页面就会出现假死的情况,所以很有必要对api进行分组。api分组,是通过在ApiConf这个配置文件中,通过@Bean注解定义一些Docket实例,网上常见的配置如下:

1
2
3
4
5
6
7
java复制代码@EnableSwagger2
publicclass ApiConfig {
@Bean
public Docket customDocket() {
return newDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
}

上述代码中通过@Bean注入一个Docket,这个配置并不是必须的,如果没有这个配置,框架会自己生成一个默认的Docket实例。这个Docket实例的作用就是指定所有它能管理的api的公共信息,比如api版本、作者等等基本信息,以及指定只列出哪些api(通过api地址或注解过滤)。

Docket实例可以有多个,比如如下代码:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
java复制代码package com.ratel.json.config;

import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.List;

import static com.google.common.collect.Lists.newArrayList;

/**
* @业务描述:
* @package_name: com.ratel.json.config
* @project_name: fast-json-test
* @author: ratelfu@qq.com
* @create_time: 2019-09-17 17:27
* @copyright (c) ratelfu 版权所有
*/

@Configuration
@EnableSwagger2
public class Swagger2GroupConfig {


/**
* 是否开启swagger,正式环境一般是需要关闭的 ,使用@value注解从application.yml中获取属性
*/
@Value("${swagger.enabled}")
private boolean enableSwagger;
@Bean
public Docket createRestApiGroup1() {

return new Docket(DocumentationType.SWAGGER_2)
.groupName("apiGroup1")
.apiInfo(apiInfo())
//是否开启 (true 开启 false隐藏。生产环境建议隐藏)
.enable(enableSwagger)
.select()
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
//包下的类,才生成接口文档
//.apis(RequestHandlerSelectors.basePackage("com.ratel.controller"))
.paths(PathSelectors.ant("/user/**"))
.build()
.securitySchemes(security());
}

@Bean
public Docket createRestApiGroup2() {

return new Docket(DocumentationType.SWAGGER_2)
.groupName("apiGroup2")
.apiInfo(apiInfo())
//是否开启 (true 开启 false隐藏。生产环境建议隐藏)
.enable(enableSwagger)
.select()
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
//包下的类,才生成接口文档
//.apis(RequestHandlerSelectors.basePackage("com.ratel.controller"))
.paths(PathSelectors.ant("/shop/**"))
.build()
.securitySchemes(security());
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot中使用Swagger2构建RESTful APIs")
.description("更多Spring Boot相关文章请关注:https://blog.csdn.net/weter_drop")
.termsOfServiceUrl("https://blog.csdn.net/weter_drop/")
.contact("ratel")
.version("1.0")
.build();
}

/**
*添加swagger的安全相关的参数,比如在请求头中添加 token
* @return
*/
private List<ApiKey> security() {
return newArrayList(
new ApiKey("token", "token", "header")
);
}

}

当在项目中配置了多个Docket实例时,也就可以对api进行分组了,比如上面代码将api分为了两组。在这种情况下,必须给每一组指定一个不同的名称,比如上面代码中的”apiGroup1”和”apiGroup2”,每一组可以用paths通过ant风格的地址表达式来指定哪一组管理哪些api。比如上面配置中,第一组管理地址为/user/开头的api第二组管理/shop/开头的api。当然,还有很多其它的过滤方式,比如跟据类注解、方法注解、地址正则表达式等等。分组后,在api 列表界面右上角的下拉选项中就可以选择不同的api组。这样就把项目的api列表分散到不同的页面了。这样,即方便管理,又不致于页面因需要加载太多api而假死。

在这里插入图片描述

本文转载自: 掘金

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

Java中的常见的设计模式总结

发表于 2021-11-27

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

  1. 你所知道的设计模式有哪些

Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

  1. 单例设计模式

最好理解的一种设计模式,分为懒汉式和饿汉式

饿汉式:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public class Singleton{  
//直接创建对象  
public static Singleton instance = newSingleton();  
//私有化构造函数  
private Singleton(){  
}  
//返回对象实例  
public static Singleton getInstance(){  
 return instance;  
}  
}

懒汉式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public class Singleton{  
//声明变量  
private static volatile Singleton singleton = null;  
//私有构造函数  
private Singleton(){  
}  
//提供对外方法  
public static Singleton getInstance(){  
 if(singleton == null){  
    synchronized(Singleton.class){  
       id(singleton == null){  
           singleton = new Singleton();  
}  
}  
}  
return singleton;  
}  
}

  1. 工厂设计模式

工厂模式分为工厂方法模式和抽象工厂模式。

工厂方法模式

工厂方法模式分为三种:

普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。

多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。

静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

  1. 建造者模式(Builder)

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象, 所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的 Test 结合起来得到的。

  1. 适配器设计模式

适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。

  1. 装饰模式

顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。

  1. 策略模式

策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无, 属于辅助类),提供辅助函数。策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

  1. 观察者模式

观察者模式很好理解,类似于邮件订阅和 RSS 订阅,当我们浏览一些博客或 wiki 时,经常会看到 RSS 图标,意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。

​

本文转载自: 掘金

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

Ubuntu1604/Hadoop313安装教程_单机

发表于 2021-11-27

Hadoop3.1.3安装教程(前期准备操作)

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻


🍕环境

本教程使用 Ubuntu 16.04 64位 作为系统环境(或者Ubuntu 14.04,Ubuntu16.04 也行,32位、64位均可),请自行安装系统。

装好了 Ubuntu 系统之后,在安装 Hadoop 前还需要做一些必备工作。

🍔创建hadoop用户

如果你安装 Ubuntu 的时候不是用的 “hadoop” 用户,那么需要增加一个名为 hadoop 的用户。

首先按 ctrl+alt+t 打开终端窗口,输入如下命令创建新用户 :

1
shell复制代码sudo useradd -m hadoop -s /bin/bash

这条命令创建就可以登陆的 hadoop 用户,并使用 /bin/bash 作为 shell。

  • sudo命令

本文中会大量使用到sudo命令。sudo是ubuntu中一种权限管理机制,管理员可以授权给一些普通用户去执行一些需要root权限执行的操作。当使用sudo命令时,就需要输入您当前用户的密码.

  • 密码

在Linux的终端中输入密码,终端是不会显示任何你当前输入的密码,也不会提示你已经输入了多少字符密码。而在windows系统中,输入密码一般都会以“*”表示你输入的密码字符

  • 输入法中英文切换

ubuntu中终端输入的命令一般都是使用英文输入。linux中英文的切换方式是使用键盘“shift”键来切换,也可以点击顶部菜单的输入法按钮进行切换。ubuntu自带的Sunpinyin中文输入法已经足够读者使用。

  • Ubuntu终端复制粘贴快捷键

在Ubuntu终端窗口中,复制粘贴的快捷键需要加上 shift,即粘贴是 ctrl+shift+v。设置完成后记得重新启动虚拟机。

image-20210917203421328

接着使用如下命令设置密码,可简单设置为 hadoop,按提示输入两次密码:

1
shell复制代码sudo passwd hadoop

image-20210917203518144

这里的密码是不可见的,直接输入密码按回车就可以。

可为 hadoop 用户增加管理员权限,方便部署,避免一些对新手来说比较棘手的权限问题:

1
shell复制代码sudo adduser hadoop sudo

image-20210917203625776

最后注销当前用户(点击屏幕右上角的齿轮)在登陆界面中选择刚创建的 hadoop 用户进行登陆。

image-20210917203717047

🍟更新apt

用 hadoop 用户登录后,我们先更新一下 apt,后续我们使用 apt 安装软件,如果没更新可能有一些软件安装不了。按 ctrl+alt+t 打开终端窗口,执行如下命令:

1
shell复制代码sudo apt-get update

image-20210917204016307

(没有此问题的跳过下面的操作)

若出现如下 “Hash校验和不符” 的提示,可通过更改软件源来解决。若没有该问题,则不需要更改。从软件源下载某些软件的过程中,可能由于网络方面的原因出现没法下载的情况,那么建议更改软件源。在学习Hadoop过程中,即使出现“Hash校验和不符”的提示,也不会影响Hadoop的安装。

image-20210917204248745

首先点击左侧任务栏的【系统设置】(齿轮图标),选择【软件和更新】

image-20210917204635099

点击 “下载自” 右侧的方框,选择【其他节点】

image-20210917204755879

在列表中选中【mirrors.aliyun.com】,并点击右下角的【选择服务器】,会要求输入用户密码,输入即可。

image-20210917204855754

接着点击关闭。

此时会提示列表信息过时,点击【重新载入】

image-20210917205000004

最后耐心等待更新缓存即可。更新完成会自动关闭【软件和更新】这个窗口。如果还是提示错误,请选择其他服务器节点如 mirrors.163.com 再次进行尝试。更新成功后,再次执行 sudo apt-get update 就正常了。

(该操作到此就结束了,没有问题的继续跟着我继续CV吧😃😃)

后续需要更改一些配置文件,我比较喜欢用的是 vim(vi增强版,基本用法相同),建议安装一下(如果你实在还不会用 vi/vim 的,请将后面用到 vim 的地方改为 gedit,这样可以使用文本编辑器进行修改,并且每次文件更改完成后请关闭整个 gedit 程序,否则会占用终端):

image-20210917205433129

和我不一样 肯定是你们没有更新,不过不要着急哈,我来帮你解决问题。根据安装的步骤,安装软件时若需要确认,在提示处输入 y 即可。

(vim简单操作指南 没有此问题的跳过下面的操作)

vim的常用模式有分为命令模式,插入模式,可视模式,正常模式。本教程中,只需要用到正常模式和插入模式。二者间的切换即可以帮助你完成本指南的学习。

  1. 正常模式
    正常模式主要用来浏览文本内容。一开始打开vim都是正常模式。在任何模式下按下Esc键就可以返回正常模式
  2. 插入编辑模式
    插入编辑模式则用来向文本中添加内容的。在正常模式下,输入i键即可进入插入编辑模式
  3. 退出vim
    如果有利用vim修改任何的文本,一定要记得保存。Esc键退回到正常模式中,然后输入:wq即可保存文本并退出vim

(该操作到此就结束了,没有问题的继续跟着我继续CV吧😃😃)

🌭安装SSH、配置SSH无密码登陆

集群、单节点模式都需要用到 SSH 登陆(类似于远程登陆,你可以登录某台 Linux 主机,并且在上面运行命令),Ubuntu 默认已安装了 SSH client,此外还需要安装 SSH server:

1
shell复制代码sudo apt-get install openssh-server

image-20210917205840642

安装软件时若需要确认,在提示处输入 y 即可。

安装完成后,可以使用如下命令登陆本机:

1
shell复制代码ssh localhost

image-20210917210328568

image-20210917210420283

但这样登陆是需要每次输入密码的,我们需要配置成SSH无密码登陆比较方便。

首先退出刚才的 ssh,就回到了我们原先的终端窗口,然后利用 ssh-keygen 生成密钥,并将密钥加入到授权中:

1
2
3
4
shell复制代码exit                           # 退出刚才的 ssh localhost
cd ~/.ssh/ # 若没有该目录,请先执行一次ssh localhost
ssh-keygen -t rsa # 会有提示,都按回车就可以
cat ./id_rsa.pub >> ./authorized_keys # 加入授权

image-20210917210552359

~的含义

在 Linux 系统中,~ 代表的是用户的主文件夹,即 “/home/用户名” 这个目录,如你的用户名为 hadoop,则 ~ 就代表 “/home/hadoop/”。 此外,命令中的 # 后面的文字是注释,只需要输入前面命令即可。

此时再用 ssh localhost 命令,无需输入密码就可以直接登陆了,如下图所示。

image-20210917210654405

🍿安装Java环境

这里就不写了可以参照Linux安装jdk1.8及配置环境变量进行Java环境的配置。

后语

厂长写博客目的初衷很简单,希望大家在学习的过程中少走弯路,多学一些东西,对自己有帮助的留下你的赞赞👍或者关注➕都是对我最大的支持,你的关注和点赞给厂长每天更文的动力。

对文章其中一部分不理解,都可以评论区回复我,我们来一起讨论,共同学习,一起进步!

微信(z613500)或者 qq(1016942589) 详细交流。

本文转载自: 掘金

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

Gin 框架:日志配置管理 介绍 安装 简述概念 快速开始

发表于 2021-11-27

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

介绍

通过一个完整例子,在 Gin 框架中合理管理日志。

有什么使用场景?

  • 日志自动滚动
  • 分成多个日志文件
  • 日志格式修改
  • 等等

我们将会使用 rk-boot 来启动 Gin 框架的微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-gin

简述概念

rk-boot 使用如下两个库管理日志。

  • zap 管理日志实例
  • lumberjack 管理日志滚动

rk-boot 定义了两种日志类型,会在后面详细介绍,这里先做个简短介绍。

  • ZapLogger: 标准日志,用于记录 Error, Info 等。
  • EventLogger: JSON 或者 Console 格式,用于记录 Event,例如 RPC 请求。

快速开始

在这个例子中,我们会试着改变 zap 日志的路径和格式。

1.创建 boot.yaml

1
2
3
4
5
6
7
8
9
10
yaml复制代码---
zapLogger:
- name: zap-log # Required
zap:
encoding: json # Optional, options: console, json
outputPaths: ["logs/zap.log"] # Optional
gin:
- name: greeter
port: 8080
enabled: true

2.创建 main.go

往 zap-log 日志实例中写个日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import (
"context"
"github.com/rookie-ninja/rk-boot"
"github.com/rookie-ninja/rk-entry/entry"
_ "github.com/rookie-ninja/rk-gin/boot"
)

func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Bootstrap
boot.Bootstrap(context.Background())

// Write zap log
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-log").GetLogger().Info("This is zap-log")

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

4.验证

文件夹结构

1
2
3
4
5
6
go复制代码├── boot.yaml
├── go.mod
├── go.sum
├── logs
│ └── zap.log
└── main.go

日志输出

1
json复制代码{"level":"INFO","ts":"2021-10-21T02:10:09.279+0800","msg":"This is zap-log"}

配置 EventLogger

上面的例子中,我们配置了 zap 日志,这回我们修改一下 EventLogger。

1.创建 boot.yaml

1
2
3
4
5
6
7
8
9
yaml复制代码---
eventLogger:
- name: event-log # Required
encoding: json # Optional, options: console, json
outputPaths: ["logs/event.log"] # Optional
gin:
- name: greeter
port: 8080
enabled: true

2.创建 main.go

往 event-log 实例中写入日志。

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
go复制代码package main

import (
"context"
"github.com/rookie-ninja/rk-boot"
_ "github.com/rookie-ninja/rk-gin/boot"
"github.com/rookie-ninja/rk-entry/entry"
)

func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Bootstrap
boot.Bootstrap(context.Background())

// Write event log
helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-log").GetEventHelper()
event := helper.Start("demo-event")
event.AddPair("key", "value")
helper.Finish(event)

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

3.启动 main.go

1
go复制代码$ go run main.go

4.验证

文件夹结构

1
2
3
4
5
6
go复制代码├── boot.yaml
├── go.mod
├── go.sum
├── logs
│ └── event.log
└── main.go

日志内容

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
json复制代码{
"endTime":"2021-11-27T01:56:56.001+0800",
"startTime":"2021-11-27T01:56:56.001+0800",
"elapsedNano":423,
"timezone":"CST",
"ids":{
"eventId":"70b034b8-27af-43ad-97a5-82c99292297d"
},
"app":{
"appName":"gin-demo",
"appVersion":"master-f948c90",
"entryName":"",
"entryType":""
},
"env":{
"arch":"amd64",
"az":"*",
"domain":"*",
"hostname":"lark.local",
"localIP":"10.8.0.2",
"os":"darwin",
"realm":"*",
"region":"*"
},
"payloads":{},
"error":{},
"counters":{},
"pairs":{
"key":"value"
},
"timing":{},
"remoteAddr":"localhost",
"operation":"demo-event",
"eventStatus":"Ended",
"resCode":"OK"
}

概念

上面的例子中,我们尝试了 ZapLogger 和 EventLogger。接下来我们看看 rk-boot 是如何实现的,并且怎么使用。

架构

ZapLoggerEntry

ZapLoggerEntry 是 zap 实例的一个封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// ZapLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is ZapLoggerEntryType.
// 3: EntryDescription: Description of ZapLoggerEntry.
// 4: Logger: zap.Logger which was initialized at the beginning.
// 5: LoggerConfig: zap.Logger config which was initialized at the beginning which is not accessible after initialization..
// 6: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type ZapLoggerEntry struct {
EntryName string `yaml:"entryName" json:"entryName"`
EntryType string `yaml:"entryType" json:"entryType"`
EntryDescription string `yaml:"entryDescription" json:"entryDescription"`
Logger *zap.Logger `yaml:"-" json:"-"`
LoggerConfig *zap.Config `yaml:"zapConfig" json:"zapConfig"`
LumberjackConfig *lumberjack.Logger `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

如何在 boot.yaml 里配置 ZapLoggerEntry?

ZapLoggerEntry 完全兼容 zap 和 lumberjack 的 YAML 结构。
用户可以根据需求,配置多个 ZapLogger 实例,并且通过 name 来访问。

完整配置:

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
yaml复制代码---
zapLogger:
- name: zap-logger # Required
description: "Description of entry" # Optional
zap:
level: info # Optional, default: info, options: [debug, DEBUG, info, INFO, warn, WARN, dpanic, DPANIC, panic, PANIC, fatal, FATAL]
development: true # Optional, default: true
disableCaller: false # Optional, default: false
disableStacktrace: true # Optional, default: true
sampling: # Optional, default: empty map
initial: 0
thereafter: 0
encoding: console # Optional, default: "console", options: [console, json]
encoderConfig:
messageKey: "msg" # Optional, default: "msg"
levelKey: "level" # Optional, default: "level"
timeKey: "ts" # Optional, default: "ts"
nameKey: "logger" # Optional, default: "logger"
callerKey: "caller" # Optional, default: "caller"
functionKey: "" # Optional, default: ""
stacktraceKey: "stacktrace" # Optional, default: "stacktrace"
lineEnding: "\n" # Optional, default: "\n"
levelEncoder: "capitalColor" # Optional, default: "capitalColor", options: [capital, capitalColor, color, lowercase]
timeEncoder: "iso8601" # Optional, default: "iso8601", options: [rfc3339nano, RFC3339Nano, rfc3339, RFC3339, iso8601, ISO8601, millis, nanos]
durationEncoder: "string" # Optional, default: "string", options: [string, nanos, ms]
callerEncoder: "" # Optional, default: ""
nameEncoder: "" # Optional, default: ""
consoleSeparator: "" # Optional, default: ""
outputPaths: [ "stdout" ] # Optional, default: ["stdout"], stdout would be replaced if specified
errorOutputPaths: [ "stderr" ] # Optional, default: ["stderr"], stderr would be replaced if specified
initialFields: # Optional, default: empty map
key: "value"
lumberjack: # Optional
filename: "rkapp-event.log" # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
maxsize: 1024 # Optional, default: 1024 (MB)
maxage: 7 # Optional, default: 7 (days)
maxbackups: 3 # Optional, default: 3 (days)
localtime: true # Optional, default: true
compress: true # Optional, default: true

如何在代码里获取 ZapLogger?

通过 name 来访问。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// Access entry
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger")

// Access zap logger
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLogger()

// Access zap logger config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLoggerConfig()

// Access lumberjack config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLumberjackConfig()

EventLoggerEntry

rk-boot 把每一个 RPC 请求看作一个 Event,并且使用 rk-query 中的 Event 类型来记录日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// EventLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is EventLoggerEntryType.
// 3: EntryDescription: Description of EventLoggerEntry.
// 4: EventFactory: rkquery.EventFactory was initialized at the beginning.
// 5: EventHelper: rkquery.EventHelper was initialized at the beginning.
// 6: LoggerConfig: zap.Config which was initialized at the beginning which is not accessible after initialization.
// 7: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type EventLoggerEntry struct {
EntryName string `yaml:"entryName" json:"entryName"`
EntryType string `yaml:"entryType" json:"entryType"`
EntryDescription string `yaml:"entryDescription" json:"entryDescription"`
EventFactory *rkquery.EventFactory `yaml:"-" json:"-"`
EventHelper *rkquery.EventHelper `yaml:"-" json:"-"`
LoggerConfig *zap.Config `yaml:"zapConfig" json:"zapConfig"`
LumberjackConfig *lumberjack.Logger `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

EventLogger 字段

我们可以看到 EventLogger 打印出来的日志里,包含字段,介绍一下这些字段。

字段 详情
endTime 结束时间
startTime 开始时间
elapsedNano Event 时间开销(Nanoseconds)
timezone 时区
ids 包含 eventId, requestId 和 traceId。如果原数据拦截器被启动,或者 event.SetRequest() 被用户调用,新的 RequestId 将会被使用,同时 eventId 与 requestId 会一模一样。 如果调用链拦截器被启动,traceId 将会被记录。
app 包含 appName, appVersion, entryName, entryType。
env 包含 arch, az, domain, hostname, localIP, os, realm, region. realm, region, az, domain 字段。这些字段来自系统环境变量(REALM,REGION,AZ,DOMAIN)。 “*“ 代表环境变量为空。
payloads 包含 RPC 相关信息。
error 包含错误。
counters 通过 event.SetCounter() 来操作。
pairs 通过 event.AddPair() 来操作。
timing 通过 event.StartTimer() 和 event.EndTimer() 来操作。
remoteAddr RPC 远程地址。
operation RPC 名字。
resCode RPC 返回码。
eventStatus Ended 或者 InProgress

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码------------------------------------------------------------------------
endTime=2021-11-27T02:30:27.670807+08:00
startTime=2021-11-27T02:30:27.670745+08:00
elapsedNano=62536
timezone=CST
ids={"eventId":"4bd9e16b-2b29-4773-8908-66c860bf6754"}
app={"appName":"gin-demo","appVersion":"master-f948c90","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.6","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/rk/v1/healthy","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:61726
operation=/rk/v1/healthy
resCode=200
eventStatus=Ended
EOE

如何在 boot.yaml 里配置 EventLoggerEntry?

EventLoggerEntry 将会把 Application 名字注入到 Event 中。启动器会从 go.mod 文件中提取 Application 名字。 如果没有 go.mod 文件,启动器会使用默认的名字。

用户可以根据需求,配置多个 EventLogger 实例,并且通过 name 来访问。

完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码---
eventLogger:
- name: event-logger # Required
description: "This is description" # Optional
encoding: console # Optional, default: console, options: console and json
outputPaths: ["stdout"] # Optional
lumberjack: # Optional
filename: "rkapp-event.log" # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
maxsize: 1024 # Optional, default: 1024 (MB)
maxage: 7 # Optional, default: 7 (days)
maxbackups: 3 # Optional, default: 3 (days)
localtime: true # Optional, default: true
compress: true # Optional, default: true

如何在代码里获取 EventLogger?

通过 name 来访问。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// Access entry
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger")

// Access event factory
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventFactory()

// Access event helper
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventHelper()

// Access lumberjack config
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetLumberjackConfig()

如何使用 Event?

Event 是一个 interface,包含了若干方法,请参考:Event

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// Get EventHelper to create Event instance
helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-log").GetEventHelper()

// Start and finish event
event := helper.Start("demo-event")
helper.Finish(event)

// Add K/V
event.AddPair("key", "value")

// Start and end timer
event.StartTimer("my-timer")
event.EndTimer("my-timer")

// Set counter
event.SetCounter("my-counter", 1)

本文转载自: 掘金

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

1…160161162…956

开发者博客

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