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

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


  • 首页

  • 归档

  • 搜索

如何在2021年通过路线图学习ASPNET Core? 大

发表于 2021-05-20

大牛镇楼

21059063.jpg

大约一个月前,我进入了ASP.NET Core学习之路。在我学习的过程中,由Moien Tajik完成的GitHub存储库和文档引起了我的注意:

ASP.NET Core开发者路线图

1_nxaMfUgEVCZ0KHC441_pQw.png

什么是ASP.NET Core?

aspnetcore-logo.png

ASP.NET是一种流行的Web开发框架,用于在.NET平台上构建Web应用程序。ASP.NET Core是ASP.NET的开源版本,可在macOS,Linux和Windows上运行。ASP.NET Core于2016年首次发布,是对ASP.NET的早期仅限Windows版本的重新设计。

了解先决条件

这是一条漫长的路。如果我在每个章节分享一点点,可能会更容易理解。

一般发展技能

数据结构与算法、GIT版本控制(VSTS,GitHub,GitLab)、HTTP / HTTPS协议、学习寻找解决方案

C#

.svg-275x300.png

了解C#9.0的基础知识、学习.NET 5、了解Dotnet CLI

一般通用技能

数据结构与算法

数据结构和算法为程序员提供了一套有效地处理数据的技术。程序员必须了解处理数据的基本概念。例如,如果程序员希望收集微博用户的详细信息,则开发者必须访问数据并使用数据结构和算法技术对其进行有效管理。

GIT版本控制(VSTS,GitHub,GitLab)

1_Jl2VDHVzFBDdXggRprziUg.png
GitHub是一个供开发人员和程序员共同使用代码的网站。

GitHub的主要好处是其版本控制系统,该版本控制系统可实现无缝协作而不会损害原始项目的完整性。GitHub上的项目是开源软件的示例。

GitLab是基于Web的DevOps生命周期工具,它使用GitLab Inc.开发的开源许可证,提供了一个Git存储库管理器,该管理器提供Wiki,问题跟踪和CI / CD管道功能。

HTTP / HTTPS协议

1161496.png
超文本传输协议(HTTP)是底层的网络协议,支持在Web上(通常在浏览器和服务器之间)传输超媒体文档,以便人们可以阅读它们。

超文本传输协议安全(HTTPS)是HTTP的安全版本,它是用于在Web浏览器和网站之间发送数据的主要协议。HTTPS被加密以提高数据传输的安全性。当特定用户通常传输敏感数据(例如通过登录银行帐户,电子邮件服务或健康保险提供商)时,这一点至关重要。

学习寻找解决方案

184956256_10159269415562838_2465061840252845430_n.jpg
大多数初学者,甚至是经验丰富的程序员都从某些资源中获得帮助。每个程序员都应了解所有这些网站,在这些网站上人们会提出棘手的编程问题,提供解决方案并互相帮助。

使用这些:

Google、博客园、知乎、掘金、StackOverflow、Reddit、Quora、Telegram/Whatsapp Groups、Coding Forums

C#

学习C#

与Java一样,C#是拥有大量活跃用户社区的最流行的编程语言之一,可以在掘金和其他在线社区上轻松地找到故障排除解决方案和编码帮助。

微软早在2001年就发布了C#语言。但是,截至2021年,C#的需求仍然很大。自.NET Core发布以来,情况尤其如此,而且这种趋势很可能会上升。

C#是Microsoft产品生态系统中最流行的编程语言。C#代码旨在快速运行并易于维护。在C#基础知识中,我们将学习如何与C#一起编写简单的程序。

你应该学习的基础知识

C#语法、类型、字符串、数字、if语句、方法等

学习.NET 5

.NET是Microsoft创建的编程平台。以下是最重要的功能:

  • 你可以用多种语言编写:C#,F#和VB.NET
  • 用.NET用不同语言编写的库可以一起工作,因为它们可以编译成IL中间代码
  • .NET 5和相关技术是开源的,它们的资源可在GitHub平台上获得
  • 在.NET 5中,你可以构建控制台应用程序,网站,API,游戏,移动应用程序和台式计算机
  • .NET非常流行。它已经与Amazon或Google技术进行了许多现成的集成,但是最简单的方法将是与Microsoft产品和Azure云一起使用。

了解Dotnet CLI

.NET命令行界面(CLI)是用于开发,构建,运行和发布.NET应用程序的跨平台工具链。
.NET CLI包括.NET SDK。若要了解如何安装.NET SDK,请参阅“安装.NET Core”。

基本命令

new restore build publish run test vstest pack migrate clean sln help store

今天就这些。感谢你的阅读。

本文转载自: 掘金

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

盘点 SpringIOC Bean 创建之 Initia

发表于 2021-05-19

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

还是来看这个图 , 来源于 @ topjava.cn/article/139…

beanSelf.jpg

前文说了前两步 :

  • 盘点 SpringIOC : Bean 创建主流程
  • 盘点 SpringIOC : Bean 创建之属性注入

这一篇来说说后面一步 InitializeBean 的相关流程 , 此篇主要包含 :

  • initializeBean 主要流程
  • 四种初始化方式的前世今生
  • initializeBean 后的参数总结

二 . InitializeBean 方法详解

2.1 BeanInitializeBean 方法 + 初始化扩展功能

上述 Bean 创建主要完成了 Bean 创建 , 属性注入,依赖处理 , 从 AbstractAutowireCapableBeanFactory # doCreateBean 发起创建流程 , 该流程后 , 实体就已经创建完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
/**
* 发起流程 , 创建主流程
**/
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
//............
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
} catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
} else {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
//............
}

2.1.1 首先是 InitializingBean 的加载流程

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
java复制代码C173- AbstractAutowireCapableBeanFactory
M173_50- initializeBean
- invokeAwareMethods(beanName, bean) :激活 Aware 方法,对特殊的 bean 处理 -> PS:M173_50_02
- applyBeanPostProcessorsBeforeInitialization : 后处理器,before
- invokeInitMethods : 激活用户自定义的 init 方法
- applyBeanPostProcessorsAfterInitialization :后处理器,after
M173_51- invokeAwareMethods -> PS:M173_51_01
?- 根据 Aware的具体类型分别设置 BeanName / BeanClassLoaderAware / BeanFactoryAware
M173_52- applyBeanPostProcessorsBeforeInitialization
?- getBeanPostProcessor 获取所有的 BeanPostProcessor 并且进行FOR 循环调用 postProcessBeforeInitialization
?- 注意 , 如果没有 Processor , 则直接返回原本的 Object bean , 存在则返回处理过的
FOR- getBeanPostProcessor
- processor.postProcessBeforeInitialization(result, beanName)
M173_53- invokeInitMethods
- 如果包含 afterPropertiesSet , 则调用 ((InitializingBean) bean).afterPropertiesSet()

// M173_50 代码
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// 激活 Aware 方法 , 此处会根据权限不同分别处理
// Step 1 : 获取系统安全接口 , 如果已经为当前应用程序建立了安全管理器,则返回该安全管理器
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}catch (Throwable ex) {
.....
}
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}

PS:M173_50_02 许可权限控制
AccessController.doPrivileged : 使用指定的AccessControlContext启用和限制的权限执行指定的PrivilegedAction。

PrivilegedAction : 此处使用的 Action , 该 Action 支持在启用特权的情况下执行逻辑

getAccessControlContext : 将访问控制上下文的创建委托给SecurityContextProvider

原因 : 这么做的主要原因是因为JVM 保护域的机制 , 当使用了 SecurityManager 时 , 并不能随意访问 . 此时类java.security.AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许 , 而使用 doPrivileged 的代码主体享有特权

initializeBean 详情

大概可以看到 , 其中重要的几个调用

  • invokeAwareMethods
  • applyBeanPostProcessorsBeforeInitialization
  • invokeInitMethods
  • applyBeanPostProcessorsAfterInitialization

我们在后面详细看看这几个方法 :

2.2 invokeAwareMethods 及 Aware 的处理

这里是为了执行 Aware 方法 , 顺便把 Aware 流程说一下

Aware 接口为 Spring 容器的核心接口,是一个具有标识作用的超级接口,实现了该接口的 bean 是具有被 Spring 容器通知的能力

用法 : 指示一个bean有资格通过一个回调样式的方法由Spring容器通知一个特定的框架对象

结构 : 通常应该只包含一个接受单个参数的返回void的方法

作用 : 被 Spring 容器通知 , 获取其中的隐藏属性

常见的 BeanAware 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码- BeanNameAware:对该 bean 对象定义的 beanName 设置到当前对象实例中
- BeanClassLoaderAware:将当前 bean 对象相应的 ClassLoader 注入到当前对象实例中
- BeanFactoryAware:BeanFactory 容器会将自身注入到当前对象实例中,这样当前对象就会拥有一个 BeanFactory 容器的引用。

// 当然,Spring 不仅仅只是提供了上面三个 Aware 接口,而是一系列:
- LoadTimeWeaverAware:加载Spring Bean时织入第三方模块,如AspectJ
- BootstrapContextAware:资源适配器BootstrapContext,如JCA,CCI
- ResourceLoaderAware:底层访问资源的加载器
- PortletConfigAware:PortletConfig
- PortletContextAware:PortletContext
- ServletConfigAware:ServletConfig
- ServletContextAware:ServletContext
- MessageSourceAware:国际化
- ApplicationEventPublisherAware:应用事件
- NotificationPublisherAware:JMX通知

在整体结构中有一个操作就是 : 检查 , 激活 Aware , 这个过程的前因后果是什么样的呢?

Aware 接口的体系非常庞大 , 我们仅以 BeanNameAware 为例

单纯的从结构上说 , 它只是一个接口

1
2
3
4
java复制代码public interface BeanNameAware extends Aware {
// 安装上文说的 , 接受单参数 ,返回 void
void setBeanName(String name);
}

那么 ,BeanNameAware 他干了什么 ?

BeanNameAware 是一个接口 , 由希望知道自己在bean工厂中的bean名称的bean实现 , 对应 Bean 依赖该接口获取其 BeanName

aware的英文意思:意识到,察觉到,发觉,发现

也就是说 , 实现了对应的 Aware 接口的类 , 才能去做相应的事情 . 也就是说 Bean 实现了 BeanNameAware , 那么就可以感知到系统为其生成的 BeanName

这也就是为什么要求 aware 中方法应该是 void 的原因 , 这里是为了回调拿到设置的值.

从代码底层看 , Aware 运行的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码C- AbstractAutowireCapableBeanFactory
M- invokeAwareMethods(final String beanName, final Object bean)
?- 该方法是在 initializeBean 中被调用

private void invokeAwareMethods(final String beanName, final Object bean) {
if (bean instanceof Aware) {

// 根据Aware 类型的不同调用对应的方法
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
ClassLoader bcl = getBeanClassLoader();
if (bcl != null) {
((BeanClassLoaderAware) bean).setBeanClassLoader(bcl);
}
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}

也就是说 , 在处理完成各种属性后 , aware 是在处理玩相关方法后 ,通过回调的方式来完善部分功能 , 例如 invokeAwareMethods 中就处理了3件事 :

  • BeanNameAware 设置Bean 名称
  • BeanClassLoaderAware 设置 BeanClassLoader
  • BeanFactoryAware 设置 Bean 工厂

Aware 的实现方式

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class CommonService implements BeanNameAware {

private String beanName;

// Spring 创建过程中 , 在生成name 后 , 会回调该接口 , 将 BeanName 注入进来 ,让对象可感知
@Override
public void setBeanName(String name) {
this.beanName = name;
}

}

2.4 applyBeanPostProcessorsBeforeInitialization

这里很好理解, 执行 BeanPostProcessors 相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}


// applyBeanPostProcessorsAfterInitialization 类似 , 其区别就是调用 postProcessAfterInitialization

2.5 invokeInitMethods 主流程

此时属性已经设置完成 , 检查bean是否实现了InitializingBean或定义了一个定制的init方法,如果实现了,则调用必要的回调

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
java复制代码// PS:M173_51_01  详情处理
protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
throws Throwable {
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
// 同理 , 对存在安全域 SecurityManager 的方法 , 通过 AccessController 进行授权调用
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
((InitializingBean) bean).afterPropertiesSet();
return null;
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}

/
if (mbd != null && bean.getClass() != NullBean.class) {
String initMethodName = mbd.getInitMethodName();
if (StringUtils.hasLength(initMethodName) &&
// 取反 , 避免反复调用
!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.isExternallyManagedInitMethod(initMethodName)) {
// 在给定bean上调用指定的自定义init方法
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}

补充一 : InitializingBean 方法

1
2
3
4
5
6
7
8
java复制代码> InitializingBean 是一个接口 , 
M- afterPropertiesSet() :
|- 在 bean 的初始化进程中会判断当前 bean 是否实现了 InitializingBean
|- 如果实现了则用 #afterPropertiesSet() 方法,进行初始化工作
|- 属性初始化的处理
|- 然后再检查是否也指定了 init-method
|- 如果指定了则通过反射机制调用指定的 init-method 方法
|- 利用反射机制执行, 激活用户自定义的初始化方法

补充二 : invokeCustomInitMethod

通常这种方式是通过 @Bean(initMethod = “initMethod”) 通过注解指定 进行处理的

  • 获取去配置的 initMethod
  • 通过反射获取方法对象
  • 方法反射的方式执行 method
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
java复制代码protected void invokeCustomInitMethod(String beanName, final Object bean, RootBeanDefinition mbd)
throws Throwable {

// 获取配置的 initMethod
String initMethodName = mbd.getInitMethodName();

// 通过反射获取方法对象
Method initMethod = (mbd.isNonPublicAccessAllowed() ?
BeanUtils.findMethod(bean.getClass(), initMethodName) :
ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName));

// 如果方法不存在 , 抛出异常或返回
if (initMethod == null) {
// 指示配置的init方法是否为默认方法
if (mbd.isEnforceInitMethod()) {
throw new BeanDefinitionValidationException(.....);
}
else {
return;
}
}

// 如果可能,为给定的方法句柄确定相应的接口方法
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod);

// 方法反射的方式执行 method , 同理 , 会获取权限管理
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
ReflectionUtils.makeAccessible(methodToInvoke);
return null;
});
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () ->
methodToInvoke.invoke(bean), getAccessControlContext());
}
catch (PrivilegedActionException pae) {
InvocationTargetException ex = (InvocationTargetException) pae.getException();
throw ex.getTargetException();
}
}
else {
try {
// 获取许可后反射获取
ReflectionUtils.makeAccessible(methodToInvoke);
methodToInvoke.invoke(bean);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

2.5 初始化方法的调用方式

在应用启动时就初始化 Bean 的方式有以下几种 :

  • 实现 InitializingBean 接口方法 afterPropertiesSet
  • 实现 ApplicationRunner 接口方法 run(ApplicationArguments args)
  • 方法标注注解 @PostConstruct
  • @Bean(initMethod = “initMethod”) 通过注解指定
1
2
3
4
5
java复制代码// 直观的从 log 上面看 , 顺序为 
- @PostConstruct
- InitializingBean
- @Bean(initMethod = "initMethod")
- ApplicationRunner

@PostConstruct 的加载流程

@PostConstruct 归属于 javax.annotation , 是 Java 原生的注解之一 , 用于需要在依赖注入完成后执行任何初始化的方法上
在类投入服务之前必须调用此方法。所有支持依赖注入的类都必须支持这个注释。即使类不请求注入任何资源,也必须调用带有PostConstruct注释的方法。

1
2
3
4
java复制代码    
C- DefaultInstanceManager # newInstance
C- DefaultInstanceManager # populateAnnotationsCache
C- DefaultInstanceManager # findPostConstruct

@Bean(initMethod = “initMethod”)

入上文说诉 , 最终会在 initializeBean -> invokeInitMethods 中执行 , 最后通过反射的方式执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码String initMethodName = mbd.getInitMethodName();

C- AbstractBeanDefinition
F- private String initMethodName;

// 对应的初始化流程
C- ConfigurationClassBeanDefinitionReader
M- loadBeanDefinitionsForBeanMethod

// 这里获取 @Bean 中的属性 initMethod
// @Bean(initMethod = "initMethod", autowire = Autowire.BY_TYPE)
String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}

// 最后在 AbstractAutowireCapableBeanFactory 中调用
C- AbstractAutowireCapableBeanFactory
M- initializeBean
M- invokeInitMethods

afterPropertiesSet

afterPropertiesSet 同样是在 invokeInitMethods 中执行

1
2
3
4
5
6
7
8
9
java复制代码
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}else {
invokeAwareMethods(beanName, bean);
}

ApplicationRunner # run 的启动流程

run 方法的执行就比较简单了, ApplicationRunner 是 SpringBoot 的专属方法 , 当 SpringApplication调用 run 方法时 , 即会执行

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
java复制代码public ConfigurableApplicationContext run(String... args) {
//..............
try {
//..............
// 执行 run 方法
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
//..............
return context;
}


private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
// 添加 ApplicationRunner 类
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 添加 CommandLineRunner 类
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
// 对所有的 runner 进行处理
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}

// 最后通过 (runner).run(args); 调用

总结

这一篇比较简单 , 但是整体效果比上一篇好 , 感觉每一篇篇幅不能太长 , 涉及的点太多就不容易捋清楚 , 也有可能导致一晚上也写不完.

附录 :

原始对象 :
image.png

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd)

initializeBean 进入前的 Object

实际上这里属性注入 ,autowired 都已经处理好了

image.png

处理完的就不发了 , 因为没有进行 aware 和 postProcess 操作 , 实际上是一样的

本文转载自: 掘金

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

一文告诉你市面上最火的游戏都是用什么引擎做的!!!

发表于 2021-05-19

点赞再看,养成赞美的习惯,微信搜一搜【香菜聊游戏】关注我

目录

1、那么什么是游戏引擎呢?

2、流行引擎盘点

3、总结:


王者荣耀是现在最火的游戏了,很多人每天都会玩个几把,但是你知道王者荣耀是用什么做的吗?

对于有些刚毕业的同学虽然有着一腔热情做游戏,但是不了解游戏行业,也不知道从何下手,也不知道怎么学,至于游戏行业的概述可以看我之前的文章,但是客户端都有哪些游戏引擎,这些引擎的特点是什么,该学习哪些呐?今天我们就详细的聊聊,希望对于想进游戏行业的程序同学一些方向。

1、那么什么是游戏引擎呢?

在盘古开天辟地的时候,所有的代码都是从“0”开始,一行一行的写出来的,简单的游戏这样做可以,但是想要快速的生产产品,这样的步骤一再重复,最终抽取出通用的代码,用来提高工业的生产效率。

观察游戏我们发现游戏中通用的功能包括UI界面的显示系统,模型的加载系统,物理引擎系统,动画系统等等,这一系列的通用功能集就叫游戏引擎。

2、流行引擎盘点

1、Cocos2d-x

官网:www.cocos.com/

代表作:神仙道,忘仙,卧龙吟,捕鱼达人1,捕鱼达人2,魔界勇士,星辰变,大掌门,小小商业街等游戏

编程语言:js,C++,lua

概述:cocos2d-x 的编程语言有不同的版本,是相对来说抽象比较少的引擎,也是我接触较少的一款游戏引擎,技术有点老了

2、cocos creator

官网:www.cocos.com/

代表作:剑与远征,保卫萝卜2,热血传奇,开心消消乐

编程语言:javascript ,typescript

概述:cocos creator 是Cocos 引擎官方团队大力推广的一款引擎,迭代速度也是非常快,使用方面也比较容易上手。

3、laya

官网:www.layabox.com/gamelist/

代表作:全民枪神边境王者,大天使之剑H5,全民打雪球

编程语言:as3, javascript,typeScript

概述:laya 是flash公司的新一代的小游戏引擎,对as 开发是友好的,但是现在似乎不温不火,前景不好

4、egret 白鹭引擎

官网:www.egret.com/

代表作:梦幻西游网页版,最强飞刀手,迷你世界创造板

编程语言:javascript

概述:很多小游戏的开发商会选用这款引擎,因为发布的包小,并且引擎易于上手。

5、unity

官网:unity.cn/madewithuni…

代表作:王者荣耀,火影忍者,原神,崩坏

编程语言:C#,Lua

概述:Unity侧重轻量级的开发,更偏向于移动端,在手游方面是非常强的,大概有70%的手游都是使用Unity开发的;自由度比较高,跨平台是最好的几乎支持所有主流平台,可以充分发挥开发者的想法和思路去做创意类型的游戏;更支持VR、AR相关应用的开发;

6、UE4

官网:www.unrealengine.com/zh-CN/

代表作:黑神话悟空,吃鸡,天刀,和平精英

编程语言:C++

概述:UE4适合重量级开发,更侧重于PC,端游以及高端手游,对于中低端手机兼容性略差;渲染效果一流,用户体验更好;引擎源代码开源;但是编程语言是C++,有点门槛,也限制了一部分unity程序转向ue4.

3、总结:

市面上的客户端引擎,但是各大游戏公司的选择是最直接反应一款引擎的特点,游戏公司使用什么,我们学什么,这样才能找到工作。

对于专注于小游戏公司,使用最多的是cocos creator 和laya,在我接触的苏州公司中 cocos creator 最多

对于中小型以上的游戏公司,使用最多的是unity,unity是现在市面上使用最广的游戏引擎,也是使用人员最多的引擎。

对于大型的游戏公司,很多都有自己的UE4 项目,大的游戏公司都在探索,是未来的主流。

在我看来最稳妥的是学习Unity,如果你想搏一搏可以选择ue4 。你会选择什么呐?

原创打字不容易,点赞,转发,关注三连,关注我公众号:【香菜聊游戏】有更多福利哦

​欢迎加入游戏开发群

本文转载自: 掘金

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

(Linux)Elasticsearch安装教程 Elast

发表于 2021-05-19

image.png Elasticsearch介绍

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。

环境

  • Linux CentOS 7.4
  • JDK 11.0.7 (JDK11下载链接)(JDK11安装教程)

安装Elasticsearch 7.12.1

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

选择LINUX X86_64:
image.png

下载后上传到服务器上,然后解压:

1
shell复制代码tar -zxvf elasticsearch-7.12.1-linux-x86_64.tar.gz

创建Linux用户

如果已有除root外的其他用户,则跳过此步骤。

由于Elasticsearch默认不支持通过root用户直接启动,所以需要创建用户,例如我的用户elastic。

然后对用户授权:

1
2
shell复制代码chown -R elastic /usr/local/software/elasticsearch-7.12.1-linux-x86_64
chmod -R 777 /usr/local/software/elasticsearch-7.12.1-linux-x86_64

Linux下用户的基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码// 创建用户testuser
useradd testuser

// 给已创建的用户testuser设置密码
passwd testuser

说明:新创建的用户会在/home下创建一个用户目录testuser

// 修改用户这个命令的相关参数
usermod --help

// 删除用户testuser
userdel testuser

// 删除用户testuser所在目录
rm -rf testuser

配置远程访问

编辑conf/elasticsearch.yml

1、修改 network.host 为 0.0.0.0

1
yml复制代码network.host: 0.0.0.0

2、修改cluster.initial_master_nodes为当前node,默认为注释,放开注释就行了

1
yml复制代码cluster.initial_master_nodes: ["node-1", "node-2"]

启动Elasticsearch

切换到非root用户,例如我自己创建的elastic用户

1
shell复制代码su elastic

进入elasticsearch-7.12.1/bin目录,执行

1
shell复制代码./elasticsearch

如果看到started,则表示启动成功

image.png

访问 http://localhost:9200/ (Elasticsearch的默认端口号是9200)

1
shell复制代码curl http://localhost:9200/

返回信息中包含了elasticsearch的版本号和lucene的版本号。

image.png

注册服务,设置开机自启动

注册服务

1
shell复制代码vim /usr/lib/systemd/system/elasticsearch.service

填入以下信息

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
plain复制代码[Unit]
Description=elasticsearch
After=network.target

[Service]
Type=forking
User=elastic
ExecStart=/usr/local/software/elasticsearch-7.12.1-linux-x86_64/bin/elasticsearch -d
PrivateTmp=true
# 指定此进程可以打开的最大文件数
LimitNOFILE=65535
# 指定此进程可以打开的最大进程数
LimitNPROC=65535
# 最大虚拟内存
LimitAS=infinity
# 最大文件大小
LimitFSIZE=infinity
# 超时设置 0-永不超时
TimeoutStopSec=0
# SIGTERM是停止java进程的信号
KillSignal=SIGTERM
# 信号只发送给给JVM
KillMode=process
# java进程不会被杀掉
SendSIGKILL=no
# 正常退出状态
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

刷新elasticsearch.service配置信息

1
shell复制代码systemctl daemon-reload

设置开机启动

1
shell复制代码systemctl enable elasticsearch.service

当返回类似Created symlink from /etc/systemd/system/multi-user.target.wants/elasticsearch.service to /usr/lib/systemd/system/elasticsearch.service.的消息时,代表注册成功!

使用systemctl管理elasticsearch.service的相关命令

1
2
3
4
5
6
7
8
9
10
shell复制代码# 启动服务
systemctl start elasticsearch.service
# 重启服务
systemctl restart elasticsearch.service
# 停止服务
systemctl stop elasticsearch.service
# 禁止开机启动
systemctl disable elasticsearch.service
# 启用开机启动
systemctl enable elasticsearch.service

遇到错误及解决方法

root用户无法启动ElasticSearch

使用root用户启动报错:

1
2
3
4
5
6
7
8
9
10
java复制代码[2021-05-19T15:01:54,427][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [izwz962mggaels00gkk8ftz] uncaught exception in thread [main]
org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root
at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:163) ~[elasticsearch-7.12.1.jar:7.12.1]
at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) ~[elasticsearch-7.12.1.jar:7.12.1]
at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:75) ~[elasticsearch-7.12.1.jar:7.12.1]
at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:116) ~[elasticsearch-cli-7.12.1.jar:7.12.1]
at org.elasticsearch.cli.Command.main(Command.java:79) ~[elasticsearch-cli-7.12.1.jar:7.12.1]
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) ~[elasticsearch-7.12.1.jar:7.12.1]
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:81) ~[elasticsearch-7.12.1.jar:7.12.1]
Caused by: java.lang.RuntimeException: can not run elasticsearch as root

切换其他用户后启动成功:

1
2
shell复制代码[root@xxx bin]# su elastic
[elastic@xxx bin]$ ./elasticsearch

参考:《ElasticSearch下用root用户无法启动问题解决》

服务器可用内存没有达到es虚拟机所需内存的默认值

使用非root账号,确保文件夹权限无误后,无法正常启动,命令行提示killed,如下:

1
2
shell复制代码[elastic@xxx bin]$ ./elasticsearch
Killed

阅读Elasticsearch官网文档后,在配置目录(/usr/local/software/elasticsearch-7.12.1/config/jvm.options.d)下添加自定义的.options文件(例如:我自己创建了jvm-heap-size.options),然后在.options文件中写入以下内容:

1
2
options复制代码-Xms256m
-Xmx256m

然后我们重新启动ElasticSearch,会发现已经能够正常启动了。

其他启动报错问题请看《Elasticsearch7.X配置远程访问》

参考文章

《Linux安装Elasticsearch并注册服务 开机自启》

《Elasticsearch7.X配置远程访问》

本文转载自: 掘金

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

Java单元测试实践笔记

发表于 2021-05-19

前言: 本文适用于已经写了一段时间单元测试,并对单元测试的合理性和有效性有进一步追求的朋友。

在编程中,测试是必不可少的一环。其中单元测试作为最初的测试,也是最小粒度的测试,是十分关键的。在这里记录一下我对Java单元测试的一些理解和实践心得。

1. 选用合理的测试框架

JUnit测试中,分为很多场景:

  • 无第三方接口调用的单元测试
  • 方法中调用了第三方接口的单元测试
  • 方法中使用了静态方法
    其中第一种情况最好解决,只需要引入JUnit依赖就好了。第二种场景,常见的处理方法是打桩和mock,这两种方法的实质都是模拟/生成一个被调用的对象,都可以对需要调用第三方接口的方法进行测试。本人更倾向于使用mockito进行mock测试,一系列工具的注解和方法会使单元测试写起来更整洁可读性更好。对于调用了静态方法的方法单元测试,可以用PowerMock进行测试。

2. 对于不同场景的输入用例枚举

代码中,我们一般要考虑不同场景的输入情况来设计我们的测试用例:

  • 调用的接口有预期内的异常情况
  • 调用的接口有预期外的异常情况(不在约定之内),也就是兜底情况
  • 输入函数的参数有误的情况
  • 输入函数的参数越界情况(比如输入列表对象约定最大是100,但是实际调用时输入了101个)
  • 测试函数中调用第三方服务时的参数正确性
    上述的场景除了最后一种都是很好理解的,其中参数越界的情况尤为注意(我在开发中经常遇到),往往我们在和前端或者是后台其他服务调用约定协议时经常忽略这一点,考虑参数越界的情况的处理是很有必要的。如果接口的定义方和使用方都不对这一点进行考虑,那么当真正遇到越界情况,可能两方的(自己认为的应该这么做)默认处理不一致,最后导致bug的产生。

3. 对于函数中被调的次数/参数正确性的测试

一般写单元测试都会忽略这一部分的重要性,写调用次数,被调第三方函数的入参确认可以有效避免很多的bug。比如在mock时候使用eq参数对函数的输入参数进行确认,在不需要执行的地方Assert.fail来保证没有执行到该步骤,在不能抛异常的测试用例中。对于调用次数,参数情况的确认建议用verify。在我review过的代码中,有些人对返回类型为void的函数不去测试,这其实是一个隐患。返回为void类型的正适合对函数中调用的第三方接口进行verify以及用eq对参数进行校验。举个例子,在一个服务中你处理完了一批数据,最后调用一个返回为void的save方法为结束,最后整个方法也返回void。那么我们是不是可以在save方法中进行verify确认函数的入参(你处理完的数据)为你期望的数据,对于你不期望的入参,调用次数verify可以设置为0,这样就有效对返回为void的函数进行了有效测试。

4. 单元测试覆盖率

比如我们团队对于单元测试覆盖率的要求是百分之90,这个覆盖率只是一个观测指标,覆盖率越高虽然不一定代表测试做好了,但是覆盖率低肯定是测试不到位,有问题的。比如你的单元测试中虽然覆盖到了某个分支路径,但是没有做好相应的verify和assert,那么这个测试就是失败的,这样的单元测试并不能保证单元的正确性。比如你修复了一个bug,发现测试代码都不用改就通过了,并且覆盖了,那么就说明之前你的测试做的不够充分,尽管覆盖了达到了,也需要补充测试用例以保证单元测试的有效性。

5. 单元测试的用例的规范

代码是给人看的,测试代码也同理,有效的测试代码注释能让读或者维护代码的人更容易理解。建议在复杂的测试场景的测试用例前添加一定的注释说明,测试函数我们一般形如:testHelloWorld_httpReturnError的命名,然后函数的上面添加java doc类型的注释说明,比如是测http状态码返回400的场景等。还有就是测试代码行数不宜过长,如果很长,建议抽取相应的私有函数,这样使得测试逻辑更加清晰。

6.测试用例

测试用例testCase的输入我们一般会抽取成一个setUp函数,进行成员的mock返回设置
如果是所有测试函数启动的前置全局设置,可以使用@Before或者@BeforeEach,一般习惯性每一个函数里面去调用setUp使得每个测试用例隔离开。如果测试用例的数据字符串过长,推荐使用文件进行读取。

7.TDD测试驱动开发

因为我的组长是测试领域的专家,在公司内部开过很多次TDD的课程,在他的耳濡目染下我也对TDD有一定的感受和理解。TDD的中文名是测试驱动开发,说通俗一点就是先有测试用例,再进行相应的开发。每次编写完测试用例,定好确定的输入输出,再去写开发代码,然后在测试用例的不断完善下逐渐完善接口。这种思想脱离了以往我们先开发再补测试的思想,先开发再测试有个很大的问题就是开发过程中的分支情况你已经想过一遍了,再编写测试用例的时候就会被限制住而有些场景没有考虑到。如果我们先写测试用例,再去迭代开发的话,更贴近于逐步描绘一个需求,不断完善需求细节的场景,这样在测试中开发,不容易漏掉细节。这种开发方式有好又怀,好处是代码的bug率大大下降,思路的转变使得在开发复杂需求的时候不容易出错。不好的地方就是因为需要时间去制定完善的测试用例,需要大量的时间来写测试和接口,还有就是需要习惯这种开发模式需要一定的时间。这种方式在我们部门业务闲的时候试行过一阵子,后面业务紧了一天要几个接口,这种模式也就没有再用了,但是不影响TDD是一个值得学习的开发模式。

本文转载自: 掘金

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

聊聊那些小而美的开源搜索引擎

发表于 2021-05-19

小而美的搜索引擎.png

今天,我们不聊ElasticSearch/Solr这样的一些比较复杂的搜索引擎,聊聊一些新晋的小而美的搜索引擎框框架。

MeiliSearch vs. Elasticsearch

Elasticsearch被设计成一个后端搜索引擎,尽管它一开始并不适合这个目的,但通常被用来为终端用户建立搜索栏。与Elasticsearch不同的是,Elasticsearch是一个通用的搜索引擎,MeiliSearch专注于提供一种特定的功能。

Elasticsearch可以处理通过海量数据的搜索并进行文本分析。为了让它有效地用于终端用户的搜索,你需要花时间去了解Elasticsearch的内部工作方式,以便能够定制和调整它来满足你的需求。MeiliSearch旨在提供针对终端用户的高性能即时搜索体验。然而,处理复杂的查询或分析非常大的数据集是不可能的。

如果你想提供一个完整的即时搜索体验,Elasticsearch有时会太慢。大多数时候,与MeiliSearch相比,它返回搜索结果的速度明显较慢。如果你需要一个简单易行的工具来部署一个容许错别字的搜索栏,提供前缀搜索功能,让用户直观地进行搜索,并以近乎完美的相关性即时返回他们的结果,MeiliSearch是一个完美的选择。

MeiliSearch vs. Algolia

MeiliSearch的灵感来自于Algolia的产品和它背后的算法。我们确实研究了他们博文中描述的大部分算法和数据结构,以便实现我们自己的算法。因此,MeiliSearch是一个基于Algolia的工作和近期研究论文的新搜索引擎。它提供了类似的功能,并且和它的前辈一样迅速地达到了相同的相关程度。

与Algolia不同的是,MeiliSearch是开源的,并且是用Rust编写的,Rust是一种系统级的现代编程语言,可以快速构建功能。Rust还实现了可移植性和灵活性,这使得我们的搜索引擎在虚拟机、容器、甚至Lambda@Edge内的部署成为一种无缝操作。

Algolia的主要资产之一是他们为客户提供的强大的全球基础设施。MeiliSearch目前提供的是一个搜索引擎,还不能提供一个有竞争力的基础设施。然而,我们的目标是使其在部署和维护方面比Algolia的要简单得多。

开放源码

Lucene

Apache Lucene是一个免费和开源的搜索库,用Java编写,用于文档的全文索引和搜索。这个项目由Doug Cutting于1999年首次创建,他之前曾在施乐公司的帕洛阿尔托研究中心(PARC)和苹果公司编写过搜索引擎。由于Lucene的开发是为了建立网络搜索应用程序,如谷歌,你可以看到DuckDuckGo仍然使用它进行一些特定的搜索。

Lucene后来被分成了几个项目。

Lucene本身:全文本搜索库。Solr:一个具有强大REST API的企业搜索服务器。Nutch:一个依靠Apache Hadoop的可扩展和可伸缩的网络爬行器。由于Lucene是许多开源或闭源搜索引擎背后的技术,它被认为是参考的搜索库。

Sonic

Sonic是一个用Rust编写的轻量级和无模式的搜索索引服务器。Sonic不能被认为是一个开箱即用的解决方案,与MeiliSearch相比,它不能保证相关性排名。事实上,它并不存储任何文档,而是由一个带有列文斯坦自动机的倒置索引组成,这意味着任何查询Sonic的应用程序都必须使用返回的ID从外部数据库检索搜索结果,然后应用一些相关度排名。它能够在几MB的RAM上运行,这使它成为数据库工具的一个极简和资源效率高的替代方案,因为数据库工具可能太过沉重而无法扩展。

#Typesense和MeiliSearch一样,Typesense是一个轻量级的开源搜索引擎,为速度而优化。我们目前正在重新评估它的特性和功能,以更好地了解它与MeiliSearch的比较。

Lucene衍生品

Lucene-Solr

Solr是Apache Lucene的一个子项目,由Yonik Seeley于2004年创建,如今是全球范围内使用最广泛的搜索引擎之一。Solr是一个搜索平台,用Java编写,并建立在Lucene之上。换句话说,Solr是一个围绕Lucene的Java API的HTTP包装器,这意味着你可以通过使用它来利用Lucene的所有功能。此外,Solr服务器与Solr云相结合,提供分布式索引和搜索功能,从而确保高可用性和可扩展性。数据是共享的,但也是自动复制的。此外,Solr不仅是一个搜索引擎;它经常被用作文档结构的NoSQL数据库。文档被存储在集合中,这可以与关系数据库中的表相媲美。

由于其可扩展的插件架构和可定制的功能,Solr是一个具有无穷无尽的使用案例的搜索引擎,尽管由于它可以索引和搜索文档和电子邮件附件,它在企业搜索中特别受欢迎。

Bleve & Tantivy

Bleve和Tantivy是搜索引擎项目,分别用Golang和Rust编写,灵感来自Apache Lucene及其算法(例如,tf-idf,术语频率-反向文档频率的缩写)。与Lucene一样,两者都是可用于任何搜索项目的库;但它们没有现成的API,无法做到开箱即用。

Elasticsearch

Elasticsearch是一个基于Lucene库的搜索引擎,在全文搜索方面最受欢迎。它提供了一个通过HTTP的JSON访问的REST API。它的一个关键选项,称为索引分片,让你有能力将索引划分为物理空间,以提高性能并确保高可用性。Lucene和Elasticsearch都是为处理大量数据流、分析日志和运行复杂查询而设计的。你可以对符合指定查询的文档进行操作和分析(例如,计算所有名为 “Thomas “的用户的平均年龄)。

今天,Lucene和Elasticsearch是开源搜索引擎领域的主导者。它们都是坚实的解决方案,适用于搜索领域的许多不同的用例,也适用于建立你自己的推荐引擎。它们是很好的通用产品,但需要正确配置才能获得与MeiliSearch或Algolia类似的结果。

商业闭源

Algolia

Algolia是一家以SaaS模式提供搜索引擎的公司。其软件是闭源的。在其早期阶段,Algolia提供可以嵌入应用程序的移动搜索引擎,面临着从头开始实施搜索算法的挑战。从一开始,就决定建立一个直接致力于最终用户的搜索引擎,即在移动应用或网站内实施搜索。Algolia在过去几年中成功地证明了容忍错别字对于改善用户体验是多么的关键,同时也证明了它对于降低跳出率和提高转化率的影响。

除了Algolia之外,搜索引擎市场上还有大量的SaaS产品可供选择。它们中的大多数使用Elasticsearch,并对其设置进行微调,以便拥有一个定制的和个性化的解决方案。

Swiftype

Swiftype是一家专门从事网站搜索和分析的搜索服务提供商。Swiftype由Matt Riley和Quin Hoxie于2012年创立,现在自2017年11月起由Elastic拥有。它是一个建立在Elasticsearch之上的端到端解决方案,意味着它有能力利用Elastic Stack。

Doofinder

Doofinder是一个付费的现场搜索服务,它的开发是为了整合到任何网站中,只需很少的配置。Doofinder被网店用来增加销售,旨在促进购买过程。

结论

每种搜索方案都最适合于特定用例的限制。由于每种类型的搜索引擎都提供了一套独特的功能,因此比较它们的性能并不容易,也不相关。例如,在Elasticsearch和Algolia之间对基于产品的数据库进行速度比较是不公平的。对于一个非常大的基于全文的数据库也是如此。

因此,我们不能将自己与基于Lucene的或其他针对特定任务的搜索引擎进行比较。

在我们所涉及的特定用例中,与MeiliSearch最相似的解决方案是Algolia。

虽然Algolia提供了最先进和最强大的搜索功能,但这种效率伴随着昂贵的定价。此外,他们的服务是面向大公司的。

MeiliSearch致力于为所有类型的开发者服务。我们的目标是提供一个对开发者友好的工具,易于安装和部署。因为为终端用户提供开箱即用的绝佳搜索体验对我们来说很重要,我们希望让每个人都能以最小的努力获得最好的搜索体验,并且不需要太大的资金投入。

本文转载自: 掘金

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

图解爬虫,用几个最简单的例子带你入门Python爬虫 一、前

发表于 2021-05-19

一、前言

爬虫一直是Python的一大应用场景,差不多每门语言都可以写爬虫,但是程序员们却独爱Python。之所以偏爱Python就是因为她简洁的语法,我们使用Python可以很简单的写出一个爬虫程序。本篇博客将以Python语言,用几个非常简单的例子带大家入门Python爬虫。

二、网络爬虫

如果把我们的因特网比作一张复杂的蜘蛛网的话,那我们的爬虫就是一个蜘,我们可以让这个蜘蛛在网上任意爬行,在网中寻找对我们有价值的“猎物”。

首先我们的网络爬虫是建立在网络之上的,所以网络爬虫的基础就是网络请求。在我们日常生活中,我们会使用浏览器浏览网页,我们在网址栏输入一个网址,点击回车在几秒时间后就能显示一个网页。在这里插入图片描述
我们表面上是点击了几个按钮,实际上浏览器帮我们完成了一些了的操作,具体操作有如下几个:

  1. 向服务器发送网络请求
  2. 浏览器接收并处理你的请求
  3. 浏览器返回你需要的数据
  4. 浏览器解析数据,并以网页的形式展现出来

我们可以将上面的过程类比我们的日常购物:

  1. 和老板说我要杯珍珠奶茶
  2. 老板在店里看看有没有你要的东西
  3. 老板拿出做奶茶的材料
  4. 老板将材料做成奶茶并给你

上面买奶茶的例子虽然有些不恰当的地方,但是我觉得已经能很好的解释什么是网络请求了。
在这里插入图片描述
在知道网络请求是什么之后,我们就可以来了解一下什么是爬虫了。实际上爬虫也是网络请求,通常情况下我们通过浏览器,而我们的爬虫则是通过程序来模拟网络请求这一过程。但是这种基础的网络请求还算不上是爬虫,爬虫通常都是有目的的。比如我想写一个爬取美女图片,我们就需要对我们请求到的数据进行一些筛选、匹配,找到对我们有价值的数据。而这一从网络请求到数据爬取这整个过程才是一个完整的爬虫。
在这里插入图片描述
有些时候网站的反爬虫做的比较差,我们可以直接在浏览器中找到它的API,我们通过API可以直接获取我们需要的数据,这种相比就要简单许多。

三、简单的爬虫

简单的爬虫就是单纯的网络请求,也可以对请求的数据进行一些简单的处理。Python提供了原生的网络请求模块urllib,还有封装版的requests模块。相比直线requests要更加方便好用,所以本文使用requests进行网络请求。

3.1、爬取一个简单的网页

在我们发送请求的时候,返回的数据多种多样,有HTML代码、json数据、xml数据,还有二进制流。我们先以百度首页为例,进行爬取:

1
2
3
4
5
6
7
8
9
python复制代码import requests
# 以get方法发送请求,返回数据
response = requests.get('http://www.baidu.com')
# 以二进制写入的方式打开一个文件
f = open('index.html', 'wb')
# 将响应的字节流写入文件
f.write(response.content)
# 关闭文件
f.close()

下面我们看看爬取的网站打开是什么样子的:

在这里插入图片描述

这就是我们熟悉的百度页面,上面看起来还是比较完整的。我们再以其它网站为例,可以就是不同的效果了,我们以CSDN为例:

在这里插入图片描述
可以看到页面的布局已经完全乱了,而且也丢失了很多东西。学过前端的都知道,一个网页是由html页面还有许多静态文件构成的,而我们爬取的时候只是将HTML代码爬取下来,HTML中链接的静态资源,像css样式和图片文件等都没有爬取,所以会看到这种很奇怪的页面。

3.2、爬取网页中的图片

首先我们需要明确一点,在爬取一些简单的网页时,我们爬取图片或者视频就是匹配出网页中包含的url信息,也就是我们说的网址。然后我们通过这个具体的url进行图片的下载,这样就完成了图片的爬取。我们有如下url:img-blog.csdnimg.cn/20200516143…,我们将这个图片url来演示下载图片的代码:

1
2
3
4
5
6
7
8
9
10
11
python复制代码import requests
# 准备url
url = 'https://img-blog.csdnimg.cn/2020051614361339.jpg'
# 发送get请求
response = requests.get(url)
# 以二进制写入的方式打开图片文件
f = open('test.jpg', 'wb')
# 将文件流写入图片
f.write(response.content)
# 关闭文件
f.close()

可以看到,代码和上面网页爬取是一样的,只是打开的文件后缀为jpg。实际上图片、视频、音频这种文件用二进制写入的方式比较恰当,而对应html代码这种文本信息,我们通常直接获取它的文本,获取方式为response.text,在我们获取文本后就可以匹配其中的图片url了。我们以下列topit.pro为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码import re
import requests
# 要爬取的网站
url = 'http://topit.pro'
# 获取网页源码
response = requests.get(url)
# 匹配源码中的图片资源
results = re.findall("<img[\\s\\S]+?src=\"(.+?)\"", response.text)
# 用于命名的变量
name = 0
# 遍历结果
for result in results:
# 在源码中分析出图片资源写的是绝对路径,所以完整url是主站+绝对路径
img_url = url+result
# 下载图片
f = open(str(name) + '.jpg', 'wb')
f.write(requests.get(img_url).content)
f.close()
name += 1

上面我们就完成了一个网站的爬取。在匹配时我们用到了正则表达式,因为正则的内容比较多,在这里就不展开了,有兴趣的读者可以自己去了解一下,这里只说一个简单的。Python使用正则是通过re模块实现的,可以调用findall匹配文本中所有符合要求的字符串。该函数传入两个参数,第一个为正则表达式,第二个为要匹配的字符串,对正则不了解的话只需要知道我们使用该正则可以将图片中的src内容拿出来。

四、使用BeautifulSoup解析HTML

BeautifulSoup是一个用来分析XML文件和HTML文件的模块,我们前面使用正则表达式进行模式匹配,但自己写正则表达式是一个比较繁琐的过程,而且容易出错。如果我们把解析工作交给BeautifulSoup会大大减少我们的工作量,在使用之前我们先安装。

4.1、BeautifulSoup的安装和简单使用

我们直接使用pip安装:

1
bash复制代码pip install beautifulsoup4

模块的导入如下:

1
python复制代码from bs4 import BeautifulSoup

下面我们就来看看BeautifulSoup的使用,我们用下面HTML文件测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img class="test" src="1.jpg">
<img class="test" src="2.jpg">
<img class="test" src="3.jpg">
<img class="test" src="4.jpg">
<img class="test" src="5.jpg">
<img class="test" src="6.jpg">
<img class="test" src="7.jpg">
<img class="test" src="8.jpg">
</body>
</html>

上面是一个非常简答的html页面,body内包含了8个img标签,现在我们需要获取它们的src,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码from bs4 import BeautifulSoup

# 读取html文件
f = open('test.html', 'r')
str = f.read()
f.close()

# 创建BeautifulSoup对象,第一个参数为解析的字符串,第二个参数为解析器
soup = BeautifulSoup(str, 'html.parser')

# 匹配内容,第一个为标签名称,第二个为限定属性,下面表示匹配class为test的img标签
img_list = soup.find_all('img', {'class':'test'})

# 遍历标签
for img in img_list:
# 获取img标签的src值
src = img['src']
print(src)

解析结果如下:

1
2
3
4
5
6
7
8
python复制代码1.jpg
2.jpg
3.jpg
4.jpg
5.jpg
6.jpg
7.jpg
8.jpg

正好就是我们需要的内容。

4.2、BeautifulSoup实战

我们可以针对网页进行解析,解析出其中的src,这样我们就可以进行图片等资源文件的爬取。下面我们用梨视频为例,进行视频的爬取。主页网址如下:www.pearvideo.com/。我们右键检查可以看到如下页面:
在这里插入图片描述
我们可以先点击1处,然后选择需要爬取的位置,比如2,在右边就会跳转到相应的位置。我们可以看到外层套了一个a标签,在我们实际操作是发现点击2的位置跳转了网页,分析出来跳转的网页应该就是a标签中的herf值。因为herf值是以/开头的,所以完整的URL应该是主站+href值,知道了这个我们就可以进行下一步的操作了,我们先从主站爬取跳转的url:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import requests
from bs4 import BeautifulSoup
# 主站
url = 'https://www.pearvideo.com/'
# 模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
}
# 发送请求
response = requests.get(url, headers=headers)
# 获取BeautifulSoup对象
soup = BeautifulSoup(response.text, 'html.parser')
# 解析出符合要求的a标签
video_list = soup.find_all('a', {'class':'actwapslide-link'})
# 遍历标签
for video in video_list:
# 获取herf并组拼成完整的url
video_url = video['href']
video_url = url + video_url
print(video_url)

输出结果如下:

1
2
3
4
5
python复制代码https://www.pearvideo.com/video_1674906
https://www.pearvideo.com/video_1674921
https://www.pearvideo.com/video_1674905
https://www.pearvideo.com/video_1641829
https://www.pearvideo.com/video_1674822

我们只爬取一个就好了,我们进入第一个网址查看源码,发现了这么一句:

1
html复制代码var contId="1674906",liveStatusUrl="liveStatus.jsp",liveSta="",playSta="1",autoPlay=!1,isLiving=!1,isVrVideo=!1,hdflvUrl="",sdflvUrl="",hdUrl="",sdUrl="",ldUrl="",srcUrl="https://video.pearvideo.com/mp4/adshort/20200517/cont-1674906-15146856_adpkg-ad_hd.mp4",vdoUrl=srcUrl,skinRes="//www.pearvideo.com/domain/skin",videoCDN="//video.pearvideo.com";

其中srcUrl就包含了视频文件的网站,但是我们肯定不能自己一个网页一个网页自己找,我们可以使用正则表达式:

1
2
3
4
5
6
7
python复制代码import re
# 获取单个视频网页的源码
response = requests.get(video_url)
# 匹配视频网址
results = re.findall('srcUrl="(.*?)"', response.text)
# 输出结果
print(results)

结果如下:

1
python复制代码['https://video.pearvideo.com/mp4/adshort/20200516/cont-1674822-14379289-191950_adpkg-ad_hd.mp4']

然后我们就可以下载这个视频了:

1
2
python复制代码with open('result.mp4', 'wb') as f:
f.write(requests.get(results[0], headers=headers).content)

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码import re
import requests
from bs4 import BeautifulSoup
# 主站
url = 'https://www.pearvideo.com/'
# 模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
}
# 发送请求
response = requests.get(url, headers=headers)
# 获取BeautifulSoup对象
soup = BeautifulSoup(response.text, 'html.parser')
# 解析出符合要求的a标签
video_list = soup.find_all('a', {'class':'actwapslide-link'})
# 遍历标签
video_url = video_list[0]['href']

response = requests.get(video_url)

results = re.findall('srcUrl="(.*?)"', response.text)

with open('result.mp4', 'wb') as f:
f.write(requests.get(results[0], headers=headers).content)

到此我们就从简单的网页到图片再到视频实现了几个不同的爬虫。

本文转载自: 掘金

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

Java 反编译工具哪家强?对比分析瞧一瞧

发表于 2021-05-19

前言

Java 反编译,一听可能觉得高深莫测,其实反编译并不是什么特别高级的操作,Java 对于 Class 字节码文件的生成有着严格的要求,如果你非常熟悉 Java 虚拟机规范,了解 Class 字节码文件中一些字节的作用,那么理解反编译的原理并不是什么问题。
甚至像下面这样的 Class 文件你都能看懂一二。

一般在逆向研究和代码分析中,反编译用到的比较多。不过在日常开发中,有时候只是简单的看一下所用依赖类的反编译,也是十分重要的。

恰好最近工作中也需要用到 Java 反编译,所以这篇文章介绍目前常见的的几种 Java 反编译工具的使用,在文章的最后也会通过编译速度、语法支持以及代码可读性三个维度,对它们进行测试,分析几款工具的优缺点。

Procyon

Github 链接:github.com/mstrobel/pr…

Procyon 不仅仅是反编译工具,它其实是专注于 Java 代码的生成和分析的一整套的 Java 元编程工具。
主要包括下面几个部分:

  • Core Framework
  • Reflection Framework
  • Expressions Framework
  • Compiler Toolset (Experimental)
  • Java Decompiler (Experimental)

可以看到反编译只是 Procyon 的其中一个模块,Procyon 原来托管于 bitbucket,后来迁移到了 GitHub,根据 GitHub 的提交记录来看,也有将近两年没有更新了。不过也有依赖 Procyon 的其他的开源反编译工具如** decompiler-procyon**,更新频率还是很高的,下面也会选择这个工具进行反编译测试。

使用 Procyon

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/org.jboss.windup.decompiler/decompiler-procyon -->
<dependency>
<groupId>org.jboss.windup.decompiler</groupId>
<artifactId>decompiler-procyon</artifactId>
<version>5.1.4.Final</version>
</dependency>

写一个简单的反编译测试。

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
java复制代码package com.wdbyte.decompiler;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;

import org.jboss.windup.decompiler.api.DecompilationFailure;
import org.jboss.windup.decompiler.api.DecompilationListener;
import org.jboss.windup.decompiler.api.DecompilationResult;
import org.jboss.windup.decompiler.api.Decompiler;
import org.jboss.windup.decompiler.procyon.ProcyonDecompiler;

/**
* Procyon 反编译测试
*
* @author https://github.com/niumoo
* @date 2021/05/15
*/
public class ProcyonTest {
public static void main(String[] args) throws IOException {
Long time = procyon("decompiler.jar", "procyon_output_jar");
System.out.println(String.format("decompiler time: %dms", time));
}
public static Long procyon(String source,String targetPath) throws IOException {
long start = System.currentTimeMillis();
Path outDir = Paths.get(targetPath);
Path archive = Paths.get(source);
Decompiler dec = new ProcyonDecompiler();
DecompilationResult res = dec.decompileArchive(archive, outDir, new DecompilationListener() {
public void decompilationProcessComplete() {
System.out.println("decompilationProcessComplete");
}
public void decompilationFailed(List<String> inputPath, String message) {
System.out.println("decompilationFailed");
}
public void fileDecompiled(List<String> inputPath, String outputPath) {
}
public boolean isCancelled() {
return false;
}
});

if (!res.getFailures().isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("Failed decompilation of " + res.getFailures().size() + " classes: ");
Iterator failureIterator = res.getFailures().iterator();
while (failureIterator.hasNext()) {
DecompilationFailure dex = (DecompilationFailure)failureIterator.next();
sb.append(System.lineSeparator() + " ").append(dex.getMessage());
}
System.out.println(sb.toString());
}
System.out.println("Compilation results: " + res.getDecompiledFiles().size() + " succeeded, " + res.getFailures().size() + " failed.");
dec.close();
Long end = System.currentTimeMillis();
return end - start;
}
}

Procyon 在反编译时会实时输出反编译文件数量的进度情况,最后还会统计反编译成功和失败的 Class 文件数量。

1
2
3
4
5
6
7
8
9
10
shell复制代码....
五月 15, 2021 10:58:28 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 650 / 783
五月 15, 2021 10:58:30 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 700 / 783
五月 15, 2021 10:58:37 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 750 / 783
decompilationProcessComplete
Compilation results: 783 succeeded, 0 failed.
decompiler time: 40599ms

Procyon GUI

对于 Procyon 反编译来说,在 GitHub 上也有基于此实现的开源 GUI 界面,感兴趣的可以下载尝试。

Github 地址:github.com/deathmarine…

CFR

GitHub 地址:github.com/leibnitz27/…

CFR 官方网站:www.benf.org/other/cfr/(可能需要FQ)

Maven 仓库:https://mvnrepository.com/artifact/org.benf/cfr

CFR(Class File Reader) 可以支持 Java 9、Java 12、Java 14 以及其他的最新版 Java 代码的反编译工作。而且 CFR 本身的代码是由 Java 6 编写,所以基本可以使用 CFR 在任何版本的 Java 程序中。值得一提的是,使用 CFR 甚至可以将使用其他语言编写的的 JVM 类文件反编译回 Java 文件。

CFR 命令行使用

使用 CFR 反编译时,你可以下载已经发布的 JAR 包,进行命令行反编译,也可以使用 Maven 引入的方式,在代码中使用。下面先说命令行运行的方式。

直接在 GitHub Tags 下载已发布的最新版 JAR. 可以直接运行查看帮助。

1
2
shell复制代码# 查看帮助
java -jar cfr-0.151.jar --help

如果只是反编译某个 class.

1
2
3
4
shell复制代码# 反编译 class 文件,结果输出到控制台
java -jar cfr-0.151.jar WindupClasspathTypeLoader.class
# 反编译 class 文件,结果输出到 out 文件夹
java -jar cfr-0.151.jar WindupClasspathTypeLoader.class --outputpath ./out

反编译某个 JAR.

1
2
3
4
5
6
7
8
shell复制代码# 反编译 jar 文件,结果输出到 output_jar 文件夹
➜ Desktop java -jar cfr-0.151.jar decompiler.jar --outputdir ./output_jar
Processing decompiler.jar (use silent to silence)
Processing com.strobel.assembler.metadata.ArrayTypeLoader
Processing com.strobel.assembler.metadata.ParameterDefinition
Processing com.strobel.assembler.metadata.MethodHandle
Processing com.strobel.assembler.metadata.signatures.FloatSignature
.....

反编译结果会按照 class 的包路径写入到指定文件夹中。

CFR 代码中使用

添加依赖这里不提。

1
2
3
4
5
6
json复制代码<!-- https://mvnrepository.com/artifact/org.benf/cfr -->
<dependency>
<groupId>org.benf</groupId>
<artifactId>cfr</artifactId>
<version>0.151</version>
</dependency>

实际上我在官方网站和 GitHub 上都没有看到具体的单元测试示例。不过没有关系,既然能在命令行运行,那么直接在 IDEA 中查看反编译后的 Main 方法入口,看下命令行是怎么执行的,就可以写出自己的单元测试了。

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.decompiler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.benf.cfr.reader.api.CfrDriver;
import org.benf.cfr.reader.util.getopt.OptionsImpl;

/**
* CFR Test
*
* @author https://github.com/niumoo
* @date 2021/05/15
*/
public class CFRTest {
public static void main(String[] args) throws IOException {
Long time = cfr("decompiler.jar", "./cfr_output_jar");
System.out.println(String.format("decompiler time: %dms", time));
// decompiler time: 11655ms
}
public static Long cfr(String source, String targetPath) throws IOException {
Long start = System.currentTimeMillis();
// source jar
List<String> files = new ArrayList<>();
files.add(source);
// target dir
HashMap<String, String> outputMap = new HashMap<>();
outputMap.put("outputdir", targetPath);

OptionsImpl options = new OptionsImpl(outputMap);
CfrDriver cfrDriver = new CfrDriver.Builder().withBuiltOptions(options).build();
cfrDriver.analyse(files);
Long end = System.currentTimeMillis();
return (end - start);
}
}

JD-Core

GiHub 地址:github.com/java-decomp…

JD-core 官方网址:java-decompiler.github.io/

JD-core 是一个的独立的 Java 库,可以用于 Java 的反编译,支持从 Java 1 至 Java 12 的字节码反编译,包括 Lambda 表达式、方式引用、默认方法等。知名的 JD-GUI 和 Eclipse 无缝集成反编译引擎就是 JD-core。
JD-core 提供了一些反编译的核心功能,也提供了单独的 Class 反编译方法,但是如果你想在自己的代码中去直接反编译整个 JAR 包,还是需要一些改造的,如果是代码中有匿名函数,Lambda 等,虽然可以直接反编译,不过也需要额外考虑。

使用 JD-core

1
2
3
4
5
6
xml复制代码        <!-- https://mvnrepository.com/artifact/org.jd/jd-core -->
<dependency>
<groupId>org.jd</groupId>
<artifactId>jd-core</artifactId>
<version>1.1.3</version>
</dependency>

为了可以反编译整个 JAR 包,使用的代码我做了一些简单改造,以便于最后一部分的对比测试,但是这个示例中没有考虑内部类,Lambda 等会编译出多个 Class 文件的情况,所以不能直接使用在生产中。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
java复制代码package com.wdbyte.decompiler;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.printer.Printer;

/**
* @author https://github.com/niumoo
* @date 2021/05/15
*/
public class JDCoreTest {

public static void main(String[] args) throws Exception {
JDCoreDecompiler jdCoreDecompiler = new JDCoreDecompiler();
Long time = jdCoreDecompiler.decompiler("decompiler.jar","jd_output_jar");
System.out.println(String.format("decompiler time: %dms", time));
}
}


class JDCoreDecompiler{

private ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();
// 存放字节码
private HashMap<String,byte[]> classByteMap = new HashMap<>();

/**
* 注意:没有考虑一个 Java 类编译出多个 Class 文件的情况。
*
* @param source
* @param target
* @return
* @throws Exception
*/
public Long decompiler(String source,String target) throws Exception {
long start = System.currentTimeMillis();
// 解压
archive(source);
for (String className : classByteMap.keySet()) {
String path = StringUtils.substringBeforeLast(className, "/");
String name = StringUtils.substringAfterLast(className, "/");
if (StringUtils.contains(name, "$")) {
name = StringUtils.substringAfterLast(name, "$");
}
name = StringUtils.replace(name, ".class", ".java");
decompiler.decompile(loader, printer, className);
String context = printer.toString();
Path targetPath = Paths.get(target + "/" + path + "/" + name);
if (!Files.exists(Paths.get(target + "/" + path))) {
Files.createDirectories(Paths.get(target + "/" + path));
}
Files.deleteIfExists(targetPath);
Files.createFile(targetPath);
Files.write(targetPath, context.getBytes());
}
return System.currentTimeMillis() - start;
}
private void archive(String path) throws IOException {
try (ZipFile archive = new JarFile(new File(path))) {
Enumeration<? extends ZipEntry> entries = archive.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
String name = entry.getName();
if (name.endsWith(".class")) {
byte[] bytes = null;
try (InputStream stream = archive.getInputStream(entry)) {
bytes = IOUtils.toByteArray(stream);
}
classByteMap.put(name, bytes);
}
}
}
}
}

private Loader loader = new Loader() {
@Override
public byte[] load(String internalName) {
return classByteMap.get(internalName);
}
@Override
public boolean canLoad(String internalName) {
return classByteMap.containsKey(internalName);
}
};

private Printer printer = new Printer() {
protected static final String TAB = " ";
protected static final String NEWLINE = "\n";
protected int indentationCount = 0;
protected StringBuilder sb = new StringBuilder();
@Override public String toString() {
String toString = sb.toString();
sb = new StringBuilder();
return toString;
}
@Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
@Override public void end() {}
@Override public void printText(String text) { sb.append(text); }
@Override public void printNumericConstant(String constant) { sb.append(constant); }
@Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); }
@Override public void printKeyword(String keyword) { sb.append(keyword); }
@Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); }
@Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); }
@Override public void indent() { this.indentationCount++; }
@Override public void unindent() { this.indentationCount--; }
@Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); }
@Override public void endLine() { sb.append(NEWLINE); }
@Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); }
@Override public void startMarker(int type) {}
@Override public void endMarker(int type) {}
};
}

JD-GUI

GitHub 地址:github.com/java-decomp…

JD-core 也提供了官方的 GUI 界面,需要的也可以直接下载尝试。

Jadx

GitHub 地址:github.com/skylot/jadx

Jadx 是一款可以反编译 JAR、APK、DEX、AAR、AAB、ZIP 文件的反编译工具,并且也配有 Jadx-gui 用于界面操作。
Jadx 使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。

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
shell复制代码git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
# 查看帮助
./build/jadx/bin/jadx --help

jadx - dex to java decompiler, version: dev

usage: jadx [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 6
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-cfg-file - deobfuscation map file, default: same dir and name as input file with '.jobf' extension
--deobf-rewrite-cfg - force to save deobfuscation map
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--rename-flags - what to rename, comma-separated, 'case' for system case sensitivity, 'valid' for java identifiers, 'printable' characters, 'none' or 'all' (default)
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--log-level - set log level, values: QUIET, PROGRESS, ERROR, WARN, INFO, DEBUG, default: PROGRESS
--version - print jadx version
-h, --help - print this help
Example:
jadx -d out classes.dex

根据 HELP 信息,如果想要反编译 decompiler.jar 到 out 文件夹。

1
2
3
4
shell复制代码./build/jadx/bin/jadx -d ./out ~/Desktop/decompiler.jar 
INFO - loading ...
INFO - processing ...
INFO - doneress: 1143 of 1217 (93%)

Fernflower

GitHub 地址:github.com/fesh0r/fern…

Fernflower 和 Jadx 一样使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码➜  fernflower-master ./gradlew build

BUILD SUCCESSFUL in 32s
4 actionable tasks: 4 executed

➜ fernflower-master java -jar build/libs/fernflower.jar
Usage: java -jar fernflower.jar [-<option>=<value>]* [<source>]+ <destination>
Example: java -jar fernflower.jar -dgs=true c:\my\source\ c:\my.jar d:\decompiled\

➜ fernflower-master mkdir out
➜ fernflower-master java -jar build/libs/fernflower.jar ~/Desktop/decompiler.jar ./out
INFO: Decompiling class com/strobel/assembler/metadata/ArrayTypeLoader
INFO: ... done
INFO: Decompiling class com/strobel/assembler/metadata/ParameterDefinition
INFO: ... done
INFO: Decompiling class com/strobel/assembler/metadata/MethodHandle
...

➜ fernflower-master ll out
total 1288
-rw-r--r-- 1 darcy staff 595K 5 16 17:47 decompiler.jar
➜ fernflower-master

Fernflower 在反编译 JAR 包时,默认反编译的结果也是一个 JAR 包。Jad

反编译速度

到这里已经介绍了五款 Java 反编译工具了,那么在日常开发中我们应该使用哪一个呢?又或者在代码分析时我们又该选择哪一个呢?我想这两种情况的不同,使用时的关注点也是不同的。如果是日常使用,读读代码,我想应该是对可读性要求更高些,如果是大量的代码分析工作,那么可能反编译的速度和语法的支持上要求更高些。
为了能有一个简单的参考数据,我使用 JMH 微基准测试工具分别对这五款反编译工具进行了简单的测试,下面是一些测试结果。

测试环境

环境变量 描述
处理器 2.6 GHz 六核Intel Core i7
内存 16 GB 2667 MHz DDR4
Java 版本 JDK 14.0.2
测试方式 JMH 基准测试。
待反编译 JAR 1 procyon-compilertools-0.5.33.jar (1.5 MB)
待反编译 JAR 2 python2java4common-1.0.0-20180706.084921-1.jar (42 MB)

反编译 JAR 1:procyon-compilertools-0.5.33.jar (1.5 MB)

Benchmark Mode Cnt Score Units
cfr avgt 10 6548.642 ± 363.502 ms/op
fernflower avgt 10 12699.147 ± 1081.539 ms/op
jdcore avgt 10 5728.621 ± 310.645 ms/op
procyon avgt 10 26776.125 ± 2651.081 ms/op
jadx avgt 10 7059.354 ± 323.351 ms/op

反编译 JAR 2: python2java4common-1.0.0-20180706.084921-1.jar (42 MB)

JAR 2 这个包是比较大的,是拿很多代码仓库合并到一起的,同时还有很多 Python 转 Java 生成的代码,理论上代码的复杂度会更高。

Benchmark Cnt Score
Cfr 1 413838.826ms
fernflower 1 246819.168ms
jdcore 1 Error
procyon 1 487647.181ms
jadx 1 505600.231ms

语法支持和可读性

如果反编译后的代码需要自己看的话,那么可读性更好的代码更占优势,下面我写了一些代码,主要是 Java 8 及以下的代码语法和一些嵌套的流程控制,看看反编译后的效果如何。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
java复制代码package com.wdbyte.decompiler;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

import org.benf.cfr.reader.util.functors.UnaryFunction;

/**
* @author https://www.wdbyte.com
* @date 2021/05/16
*/
public class HardCode <A, B> {
public HardCode(A a, B b) { }

public static void test(int... args) { }

public static void main(String... args) {
test(1, 2, 3, 4, 5, 6);
}

int byteAnd0() {
int b = 1;
int x = 0;
do {
b = (byte)((b ^ x));
} while (b++ < 10);
return b;
}

private void a(Integer i) {
a(i);
b(i);
c(i);
}

private void b(int i) {
a(i);
b(i);
c(i);
}

private void c(double d) {
c(d);
d(d);
}

private void d(Double d) {
c(d);
d(d);
}

private void e(Short s) {
b(s);
c(s);
e(s);
f(s);
}

private void f(short s) {
b(s);
c(s);
e(s);
f(s);
}

void test1(String path) {
try {
int x = 3;
} catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) { return; }
throw t;
} finally {
System.out.println("Fred");
if (path == null) { throw new IllegalStateException(); }
}
}

private final List<Integer> stuff = new ArrayList<>();{
stuff.add(1);
stuff.add(2);
}

public static int plus(boolean t, int a, int b) {
int c = t ? a : b;
return c;
}

// Lambda
Integer lambdaInvoker(int arg, UnaryFunction<Integer, Integer> fn) {
return fn.invoke(arg);
}

// Lambda
public int testLambda() {
return lambdaInvoker(3, x -> x + 1);
// return 1;
}

// Lambda
public Integer testLambda(List<Integer> stuff, int y, boolean b) {
return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);
}

// stream
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream()
.filter(x -> {
System.out.println(x);
return x.intValue() / 2 == 0;
})
.map(x -> (Integer)x+2)
.mapToInt(x -> x);
s.toArray();
}

// switch
public void testSwitch1(){
int i = 0;
switch(((Long)(i + 1L)) + "") {
case "1":
System.out.println("one");
}
}
// switch
public void testSwitch2(String string){
switch (string) {
case "apples":
System.out.println("apples");
break;
case "pears":
System.out.println("pears");
break;
}
}

// switch
public static void testSwitch3(int x) {
while (true) {
if (x < 5) {
switch ("test") {
case "okay":
continue;
default:
continue;
}
}
System.out.println("wow x2!");
}
}
}

此处本来贴出了所有工具的反编译结果,但是碍于文章长度和阅读体验,没有放出来,不过我在个人博客的发布上是有完整代码的,个人网站排版比较自由,可以使用 Tab 选项卡的方式展示。如果需要查看可以访问 www.wdbyte.com 进行查看。

Procyon

看到 Procyon 的反编译结果,还是比较吃惊的,在正常反编译的情况下,反编译后的代码基本上都是原汁原味。唯一一处反编译后和源码语法上有变化的地方,是一个集合的初始化操作略有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 源码
public HardCode(A a, B b) { }
private final List<Integer> stuff = new ArrayList<>();{
stuff.add(1);
stuff.add(2);
}
// Procyon 反编译
private final List<Integer> stuff;

public HardCode(final A a, final B b) {
(this.stuff = new ArrayList<Integer>()).add(1);
this.stuff.add(2);
}

而其他部分代码, 比如装箱拆箱,Switch 语法,Lambda 表达式,流式操作以及流程控制等,几乎完全一致,阅读没有障碍。

装箱拆箱操作反编译后完全一致,没有多余的类型转换代码。

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
java复制代码// 源码
private void a(Integer i) {
a(i);
b(i);
c(i);
}

private void b(int i) {
a(i);
b(i);
c(i);
}

private void c(double d) {
c(d);
d(d);
}

private void d(Double d) {
c(d);
d(d);
}

private void e(Short s) {
b(s);
c(s);
e(s);
f(s);
}

private void f(short s) {
b(s);
c(s);
e(s);
f(s);
}
// Procyon 反编译
private void a(final Integer i) {
this.a(i);
this.b(i);
this.c(i);
}

private void b(final int i) {
this.a(i);
this.b(i);
this.c(i);
}

private void c(final double d) {
this.c(d);
this.d(d);
}

private void d(final Double d) {
this.c(d);
this.d(d);
}

private void e(final Short s) {
this.b(s);
this.c(s);
this.e(s);
this.f(s);
}

private void f(final short s) {
this.b(s);
this.c(s);
this.e(s);
this.f(s);
}

Switch 部分也是一致,流程控制部分也没有变化。

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
java复制代码// 源码 switch
public void testSwitch1(){
int i = 0;
switch(((Long)(i + 1L)) + "") {
case "1":
System.out.println("one");
}
}
public void testSwitch2(String string){
switch (string) {
case "apples":
System.out.println("apples");
break;
case "pears":
System.out.println("pears");
break;
}
}
public static void testSwitch3(int x) {
while (true) {
if (x < 5) {
switch ("test") {
case "okay":
continue;
default:
continue;
}
}
System.out.println("wow x2!");
}
}
// Procyon 反编译
public void testSwitch1() {
final int i = 0;
final String string = (Object)(i + 1L) + "";
switch (string) {
case "1": {
System.out.println("one");
break;
}
}
}
public void testSwitch2(final String string) {
switch (string) {
case "apples": {
System.out.println("apples");
break;
}
case "pears": {
System.out.println("pears");
break;
}
}
}
public static void testSwitch3(final int x) {
while (true) {
if (x < 5) {
final String s = "test";
switch (s) {
case "okay": {
continue;
}
default: {
continue;
}
}
}
else {
System.out.println("wow x2!");
}
}
}

Lambda 表达式和流式操作完全一致。

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
java复制代码// 源码
// Lambda
public Integer testLambda(List<Integer> stuff, int y, boolean b) {
return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);
}

// stream
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream()
.filter(x -> {
System.out.println(x);
return x.intValue() / 2 == 0;
})
.map(x -> (Integer)x+2)
.mapToInt(x -> x);
s.toArray();
}
// Procyon 反编译
public Integer testLambda(final List<Integer> stuff, final int y, final boolean b) {
return stuff.stream().filter(b ? (x -> x > y) : (x -> x < 3)).findFirst().orElse(null);
}

public static <Y extends Integer> void testStream(final List<Y> list) {
final IntStream s = list.stream().filter(x -> {
System.out.println(x);
return x / 2 == 0;
}).map(x -> x + 2).mapToInt(x -> x);
s.toArray();
}

流程控制,反编译后发现丢失了无意义的代码部分,阅读来说并无障碍。

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
java复制代码// 源码
void test1(String path) {
try {
int x = 3;
} catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) { return; }
throw t;
} finally {
System.out.println("Fred");
if (path == null) { throw new IllegalStateException(); }
}
}
// Procyon 反编译
void test1(final String path) {
try {}
catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) {
return;
}
throw t;
}
finally {
System.out.println("Fred");
if (path == null) {
throw new IllegalStateException();
}
}
}

鉴于代码篇幅,下面几种的反编译结果的对比只会列出不同之处,相同之处会直接跳过。

CFR

CFR 的反编译结果多出了类型转换部分,个人来看没有 Procyon 那么原汁原味,不过也算是十分优秀,测试案例中唯一不满意的地方是对 while continue 的处理。

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
java复制代码// CFR 反编译结果
// 装箱拆箱
private void e(Short s) {
this.b(s.shortValue()); // 装箱拆箱多出了类型转换部分。
this.c(s.shortValue()); // 装箱拆箱多出了类型转换部分。
this.e(s);
this.f(s);
}
// 流程控制
void test1(String path) {
try {
int n = 3;// 流程控制反编译结果十分满意,原汁原味,甚至此处的无意思代码都保留了。
}
catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) {
return;
}
throw t;
}
finally {
System.out.println("Fred");
if (path == null) {
throw new IllegalStateException();
}
}
}
// Lambda 和 Stream 操作完全一致,不提。
// switch 处,反编译后功能一致,但是流程控制有所更改。
public static void testSwitch3(int x) {
block6: while (true) { // 源码中只有 while(true),反编译后多了 block6
if (x < 5) {
switch ("test") {
case "okay": {
continue block6; // 多了 block6
}
}
continue;
}
System.out.println("wow x2!");
}
}

JD-Core

JD-Core 和 CFR 一样,对于装箱拆箱操作,反编译后不再一致,多了类型转换部分,而且自动优化了数据类型。个人感觉,如果是反编译后自己阅读,通篇的数据类型的转换优化影响还是挺大的。

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复制代码// JD-Core 反编译
private void d(Double d) {
c(d.doubleValue()); // 新增了数据类型转换
d(d);
}

private void e(Short s) {
b(s.shortValue()); // 新增了数据类型转换
c(s.shortValue()); // 新增了数据类型转换
e(s);
f(s.shortValue()); // 新增了数据类型转换
}

private void f(short s) {
b(s);
c(s);
e(Short.valueOf(s)); // 新增了数据类型转换
f(s);
}
// Stream 操作中,也自动优化了数据类型转换,阅读起来比较累。
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream().filter(x -> {
System.out.println(x);
return (x.intValue() / 2 == 0);
}).map(x -> Integer.valueOf(x.intValue() + 2)).mapToInt(x -> x.intValue());
s.toArray();
}

Jadx

首先 Jadx 在反编译测试代码时,报出了错误,反编译的结果里也有提示不能反编 Lambda 和 Stream 操作,反编译结果中变量名称杂乱无章,流程控制几乎阵亡,如果你想反编译后生物肉眼阅读,Jadx 肯定不是一个好选择。

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
java复制代码// Jadx 反编译
private void e(Short s) {
b(s.shortValue());// 新增了数据类型转换
c((double) s.shortValue());// 新增了数据类型转换
e(s);
f(s.shortValue());// 新增了数据类型转换
}

private void f(short s) {
b(s);
c((double) s);// 新增了数据类型转换
e(Short.valueOf(s));// 新增了数据类型转换
f(s);
}
public int testLambda() { // testLambda 反编译失败
/*
r2 = this;
r0 = 3
r1 = move-result
java.lang.Integer r0 = r2.lambdaInvoker(r0, r1)
int r0 = r0.intValue()
return r0
*/
throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testLambda():int");
}
// Stream 反编译失败
public static <Y extends java.lang.Integer> void testStream(java.util.List<Y> r3) {
/*
java.util.stream.Stream r1 = r3.stream()
r2 = move-result
java.util.stream.Stream r1 = r1.filter(r2)
r2 = move-result
java.util.stream.Stream r1 = r1.map(r2)
r2 = move-result
java.util.stream.IntStream r0 = r1.mapToInt(r2)
r0.toArray()
return
*/
throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testStream(java.util.List):void");
}
public void testSwitch2(String string) { // switch 操作无法正常阅读,和源码出入较大。
char c = 65535;
switch (string.hashCode()) {
case -1411061671:
if (string.equals("apples")) {
c = 0;
break;
}
break;
case 106540109:
if (string.equals("pears")) {
c = 1;
break;
}
break;
}
switch (c) {
case 0:
System.out.println("apples");
return;
case 1:
System.out.println("pears");
return;
default:
return;
}
}

Fernflower

Fernflower 的反编译结果总体上还是不错的,不过也有不足,它对变量名称的指定,以及 Switch 字符串时的反编译结果不够理想。

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
java复制代码//反编译后变量命名不利于阅读,有很多 var 变量
int byteAnd0() {
int b = 1;
byte x = 0;

byte var10000;
do {
int b = (byte)(b ^ x);
var10000 = b;
b = b + 1;
} while(var10000 < 10);

return b;
}
// switch 反编译结果使用了hashCode
public static void testSwitch3(int x) {
while(true) {
if (x < 5) {
String var1 = "test";
byte var2 = -1;
switch(var1.hashCode()) {
case 3412756:
if (var1.equals("okay")) {
var2 = 0;
}
default:
switch(var2) {
case 0:
}
}
} else {
System.out.println("wow x2!");
}
}
}

总结

五种反编译工具比较下来,结合反编译速度和代码可读性测试,看起来 CFR 工具胜出,Procyon 紧随其后。CFR 在速度上不落下风,在反编译的代码可读性上,是最好的,主要体现在反编译后的变量命名、装箱拆箱、类型转换,流程控制上,以及对 Lambda 表达式、Stream 流式操作和 Switch 的语法支持上,都非常优秀。根据 CFR 官方介绍,已经支持到 Java 14 语法,而且截止写这篇测试文章时,CFR 最新提交代码时间实在 11 小时之前,更新速度很快。

文章中部分代码已经上传 GitHub :github.com/niumoo/lab-…

最后的话

文章有帮助可以点个「赞」或「分享」,都是支持,我都喜欢!

文章每周持续更新,,可以关注「 未读代码 」公众号或者我的博客,也可以加我微信:wn8398。

本文转载自: 掘金

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

简述HashMap的扩容机制(JDK7 和JDK8 对比)|

发表于 2021-05-19

本文正在参加「Java主题月 - Java 刷题打卡」,详情查看 活动链接

名词简述:

  • capacity 容量,默认16
  • loadFactor 负载因子,默认0.75
  • threshold 阈值 阈值=容量*负载因子,默认12,空参构造hashMap时。当map中的元素个数大于阈值时会触发扩容

什么情况下会扩容 (JDK7和JDK8的情况不同)

  • 一般情况下,在元素个数大于阈值时会发生扩容,每次扩容的容量都是之前容量的两倍。
  • HashMap的容量是有上线的,容量最大为1>>3​0,自行百度计算大小。超过了这个值则不会再增长,并且阈值会设置成,即永远都不会超过阈值

对比JDK7 和JKD8 的扩容机制

JDK7的扩容机制相对简单,有以下特性:

  • 空参构造函数:以默认容量、默认负载因子、默认阈值初始化数组,内部数组是空数组
  • 有参构造函数:根据参数确定容量、负载因子、阈值等
  • 第一次put时才会初始化数组,其容量变为不小于指定容量的2的幂数。然后根据负载因子确定阈值
  • 如果不是第一次扩容,则新容量=旧容量x2 新阈值 = 新容量x负载因子

注意:
jdk7扩容的条件:只有在数组容量大于阈值时 并且 **发生了哈希冲突**时,才会进行扩容
image.png
则会出现下属情况:

  1. 假设默认长度为16的hashMap,阈值为12,负载因子为0.75。此时加入第12个或者第13个元素的时候没有发生哈希冲突,则不会发生扩容。在不发生哈希冲突的情况下,默认初始化的hashMap最多可以存16个元素。
  2. 同默认初始化的HashMap,可能存放最多26(11+15)个元素。前11个元素全部发生哈希冲突存放在同一个位置,此时元素个数为11,不超过12,不会发生扩容,后续15个元素全部存放在其他位置,此时因为新加入的元素没有发生哈希冲突,所以不会发生扩容。当加入第27个元素时,必定会发生哈希冲突,并且元素个数大于阈值发生扩容

​

​

JDK8的扩容机制 (做了许多调整)

  1. JDK8中的hashMap扩容只需要满足一个条件:当存放的新值(注意不是替换已有元素的位置时)的时候已有的元素个数大于阈值(已有元素等于阈值,下一个存放必然触发扩容机制)

注:
(1) 扩容一定是放入新值的时候,该新值不是替换以前的位置的情况下。
(2)扩容发生在存放元素之后,当数据存放之后(先存放,后扩容), 判断当前存入对象的个数,如果大于阈值则进行扩容
​

  1. HashMap的容量变化通常存在以下几种情况:
  • 空参构造函数:实例化的HashMap默认内部数组时null,即没有实例化。第一次调用put方法时,才会开始第一次初始化扩容,长度为16
  • 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数赋值给**阈值。 **第一次调用put方法时,会将阈值复制给容量,然后让阈值 = 容量x负载因子。(因此并不是我们手动指定了容量就一定不会出发扩容,超过阈值后一样会扩容!!)
  • 如果不是第一次扩容,则容量变为原来的两倍,阈值也变为原来的两倍

注意:

  • 首次put时,会先触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容
  • 不是首次put,则不会再初始化,直接存入数据,然后判断是否需要扩容

背景知识

Java7中HashMap底层采用的时Entry对数组,而每个Entry对又向下延伸时一个链表,在链表的每一个Entry对上不仅存放着自己的K/V值,还存放了前一个和后一个Entry对的地址
Java8中的HashMap底层结构有一定的变化,还是使用的数组,但是数组的对象啊以前时Entry对,现在换成了Node对象(可以理解是Entry对,结构一样,存储时也会存K/V键值对,前一个和后一个的Node的地址),以前的所有Entry向下延伸都是链表,Java8变成了链表和红黑树的组合,数据少量存入的时候优先还是链表,当链表长度大于8,且总数据量大于64时,链表会转化为红黑树,所以Java8的数据存储是链表+红黑树的组合。如果数据量小于64则只有链表,如果数据量大于64,且某一个数组下标数据量大于8,那么该处即为红黑树。
​

思考:

  1. Java8的扩容机制上相对于Java7少了哈希冲突的判断,则会出现全部发生哈希冲突的时候,导致可能出现12个元素在hashMap数组的同一个位置。那么对于Java8来说一个好的散列算法很重要,需要分布均匀
  2. 为什么负载因子是0.75

最后:
本篇文章是通过百度搜索内容进行汇总的!有写的不对的地方,欢迎指正,希望能给你带来帮助~

参考链接1:zhuanlan.zhihu.com/p/114363420

参考链接2:blog.csdn.net/pange1991/a…

本文转载自: 掘金

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

hive学习笔记之三:内部表和外部表

发表于 2021-05-19

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

《hive学习笔记》系列导航

  1. 基本数据类型
  2. 复杂数据类型
  3. 内部表和外部表
  4. 分区表
  5. 分桶
  6. HiveQL基础
  7. 内置函数
  8. Sqoop
  9. 基础UDF
  10. 用户自定义聚合函数(UDAF)
  11. UDTF

本篇概览

  • 本文是《hive学习笔记》系列的第三篇,要学习的是各种类型的表及其特点,主要内容如下:
  1. 建库
  2. 内部表(也叫管理表或临时表)
  3. 外部表
  4. 表的操作
    接下来从最基本的建库开始

建库

  1. 创建名为test的数据库(仅当不存在时才创建),添加备注信息test database:
1
2
sql复制代码create database if not exists test 
comment 'this is a database for test';
  1. 查看数据库列表(名称模糊匹配):
1
2
3
4
5
sql复制代码hive> show databases like 't*';
OK
test
test001
Time taken: 0.016 seconds, Fetched: 2 row(s)
  1. describe database命令查看此数据库信息:
1
2
3
4
sql复制代码hive> describe database test;
OK
test this is a database for test hdfs://node0:8020/user/hive/warehouse/test.db hadoop USER
Time taken: 0.035 seconds, Fetched: 1 row(s)
  1. 上述命令可见,test数据库在hdfs上的存储位置是hdfs://node0:8020/user/hive/warehouse/test.db,打开hadoop的web页面,查看hdfs目录,如下图,该路径的文件夹已经创建,并且是以.db结尾的:

在这里插入图片描述
5. 新建数据库的文件夹都在/user/hive/warehouse下面,这是在中配置的,如下图红框:

在这里插入图片描述
6. 删除数据库,加上if exists,当数据库不存在时,执行该语句不会返回Error:

1
2
3
sql复制代码hive> drop database if exists test;
OK
Time taken: 0.193 seconds

以上就是常用的库相关操作,接下来实践表相关操作;

内部表

  1. 按照表数据的生命周期,可以将表分为内部表和外部表两类;
  2. 内部表也叫管理表或临时表,该类型表的生命周期时由hive控制的,默认情况下数据都存放在/user/hive/warehouse/下面;
  3. 删除表时数据会被删除;
  4. 以下命令创建的就是内部表,可见前面两篇文章中创建的表都是内部表:
1
2
3
sql复制代码create table t6(id int, name string)
row format delimited
fields terminated by ',';
  1. 向t6表新增一条记录:
1
sql复制代码insert into t6 values (101, 'a101');
  1. 使用hadoop命令查看hdfs,可见t6表有对应的文件夹,里面的文件保存着该表数据:
1
2
3
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -ls /user/hive/warehouse/t6
Found 1 items
-rwxr-xr-x 3 hadoop supergroup 9 2020-10-31 11:14 /user/hive/warehouse/t6/000000_0
  1. 查看这个000000_0文件的内容,如下可见,就是表内的数据:
1
2
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -cat /user/hive/warehouse/t6/000000_0
101 a101
  1. 执行命令drop table t6;删除t6表,再次查看t6表对应的文件,发现整个文件夹都不存在了:
1
2
3
4
5
6
7
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -ls /user/hive/warehouse/
Found 5 items
drwxr-xr-x - hadoop supergroup 0 2020-10-27 20:42 /user/hive/warehouse/t1
drwxr-xr-x - hadoop supergroup 0 2020-10-29 00:13 /user/hive/warehouse/t2
drwxr-xr-x - hadoop supergroup 0 2020-10-29 00:14 /user/hive/warehouse/t3
drwxr-xr-x - hadoop supergroup 0 2020-10-29 13:04 /user/hive/warehouse/t4
drwxr-xr-x - hadoop supergroup 0 2020-10-29 16:47 /user/hive/warehouse/t5

外部表

  1. 创建表的SQL语句中加上external,创建的就是外部表了;
  2. 外部表的数据生命周期不受Hive控制;
  3. 删除外部表的时候不会删除数据;
  4. 外部表的数据,可以同时作为多个外部表的数据源共享使用;
  5. 接下来开始实践,下面是建表语句:
1
2
3
4
sql复制代码create external table t7(id int, name string)
row format delimited
fields terminated by ','
location '/data/external_t7';
  1. 查看hdfs文件,可见目录/data/external_t7/已经创建:
1
2
3
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -ls /data/
Found 1 items
drwxr-xr-x - hadoop supergroup 0 2020-10-31 12:02 /data/external_t7
  1. 新增一条记录:
1
sql复制代码insert into t7 values (107, 'a107');
  1. 在hdfs查看t7表对应的数据文件,可以见到新增的内容:
1
2
3
4
5
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -ls /data/external_t7
Found 1 items
-rwxr-xr-x 3 hadoop supergroup 9 2020-10-31 12:06 /data/external_t7/000000_0
[hadoop@node0 bin]$ ./hadoop fs -cat /data/external_t7/000000_0
107,a107
  1. 试试多个外部表共享数据的功能,执行以下语句再建个外部表,名为t8,对应的存储目录和t7是同一个:
1
2
3
4
sql复制代码create external table t8(id_t8 int, name_t8 string)
row format delimited
fields terminated by ','
location '/data/external_t7';
  1. 建好t8表后立即查看数据,发现和t7表一模一样,可见它们已经共享了数据:
1
2
3
4
5
6
7
8
sql复制代码hive> select * from t8;
OK
107 a107
Time taken: 0.068 seconds, Fetched: 1 row(s)
hive> select * from t7;
OK
107 a107
Time taken: 0.074 seconds, Fetched: 1 row(s)
  1. 接下来删除t7表,再看t8表是否还能查出数据,如下可见,数据没有被删除,可以继续使用:
1
2
3
4
5
6
7
sql复制代码hive> drop table t7;
OK
Time taken: 1.053 seconds
hive> select * from t8;
OK
107 a107
Time taken: 0.073 seconds, Fetched: 1 row(s)
  1. 把t8表也删掉,再去看数据文件,如下所示,依然存在:
1
2
shell复制代码[hadoop@node0 bin]$ ./hadoop fs -cat /data/external_t7/000000_0
107,a107
  1. 可见外部表的数据不会在删除表的时候被删除,因此,在实际生产业务系统开发中,外部表是我们主要应用的表类型;

表的操作

  1. 再次创建t8表:
1
2
3
sql复制代码create table t8(id int, name string)
row format delimited
fields terminated by ',';
  1. 修改表名:
1
sql复制代码alter table t8 rename to t8_1;
  1. 可见修改表名已经生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
slq复制代码hive> alter table t8 rename to t8_1;
OK
Time taken: 0.473 seconds
hive> show tables;
OK
alltype
t1
t2
t3
t4
t5
t6
t8_1
values__tmp__table__1
values__tmp__table__2
Time taken: 0.029 seconds, Fetched: 10 row(s)
  1. 添加字段:
1
sql复制代码alter table t8_1 add columns(remark string);

查看表结构,可见已经生效:

1
2
3
4
5
6
sql复制代码hive> desc t8_1;
OK
id int
name string
remark string
Time taken: 0.217 seconds, Fetched: 3 row(s)

至此,咱们对内部表和外部表已经有了基本了解,接下来的文章学习另一种常见的表类:分区表;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

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

1…665666667…956

开发者博客

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