Giter Site home page Giter Site logo

关于单例 about kotlin-tutorials HOT 18 CLOSED

bennyhuo avatar bennyhuo commented on June 28, 2024
关于单例

from kotlin-tutorials.

Comments (18)

wafer-li avatar wafer-li commented on June 28, 2024

实际上,Java 单例的最佳实践是使用 枚举 来实现单例模式。

详情请看:https://segmentfault.com/a/1190000000699591

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

刚才试了一下,Kotlin object 的反编译代码如下:

public final class Test {
   public static final Test INSTANCE;

   private Test() {
      INSTANCE = (Test)this;
   }

   static {
      new Test();
   }
}

实际上已经做到了静态内部类的功能,而不是单纯的 PlainOldSingleton

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024

赞,看来是编译器更新了。。。真是防不胜防啊,我录制的时候应该还是 1.0.4 还是 1.0.6 。。。不记得了

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024

序列化和反序列化确实是个注意的点,不过,有点儿吹毛求疵了,一般情况下不会怎么提到。

既然说起来,我很早读 Effective Java 的时候就看到这个点,我一直很奇怪什么人会去攻击一个单例,因为只要你拿到 Runtime,你总是有办法搞出新的实例,比如 Unsafe。。如果你有答案,千万别吝啬,告诉我,哈哈

无论如何,还是得谢谢你指出~

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

单例通常作为程序的全局访问点,可能保存有比较多的状态。

攻击者拿到单例,基本上就相当于拿到了程序的全局状态;

比如说,我有一个单例的 RetrofitApiClient,但是被反射攻击之后,把我的 baseUrl 改了;

这样就神不知鬼不觉的把 auth key 什么的都泄露了。

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

enum 使用反射实例化会导致 编译错误,详情请看这里

这样,使用枚举,就可以防止序列化和反射攻击了。

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024
  1. 静态内部类的问题,实际上 object 现在仍然是 plainOld哈:
public final class Test {
   public static final Test INSTANCE;

   private Test() {
      INSTANCE = (Test)this;
   }

   static {
      new Test();
   }
}

你仔细看这段代码,实例化时在何时? 静态块的执行与直接给 INSTANCE 赋值没有区别。

昨天在手机上没有仔细看,编译器应该是不会修改这个逻辑的,不然在 Java 中引用静态内部类的实例可能会比较麻烦。

  1. 你是说攻击者侵入你的运行时,通过修改你的单例的实例来达到攻击的目的?如果攻击者真的侵入你的运行时,想知道你的什么都很容易啊,拿你原来的单例实例直接获取不就完了,还需要修改你的单例实例?

我仍然看不太出来把单例做的这么安全有什么实际的价值,除了学术或者技术上面~ 希望能继续为我解惑,多谢!

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

第一点当时看的太粗糙了,不好意思;

当时只是觉得,Kotlin 作为一个 Better Java,不应该连实现单例这样简单的模式还有那么复杂的写法;
单例就应该是 object,这样才做到了对 Java 的尽量简化。

第二点实际上我也没有想明白,不过枚举实现足够简单,而且还有额外的一些特性,所以就直接用了,现在看来似乎没有必要防止反射攻击;

不过在 SO 上的单例问题或多或少都提到了反射攻击,应该还是有一定用处的。

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

重新查了一下资料,第二个问题可以解决了:

问题的关键在于:『反射攻击』这个究竟是什么意思,它不是『攻击』的意思,而是说,反射的合法使用会造成单例模式失效

常见的反射攻击就是 对象的序列化,序列化实际上使用了反射来生成一个新的实例,然后去检查 readResolve 方法;

所以,当你的单例要实现对象的序列化的时候,如果没有 readResolve 方法,就会导致单例失效。

而且,即使有了 readResolve 方法,也不一定就能保证单例就是单例,Effectvie Java 在 77 条做了一个说明:

如果依赖 readResolve 进行实例控制,带有对象引用类型的实例域则都必须声明为 transient

但是,枚举类型自己实现了一套序列化机制,防止了这种事情的发生。

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024

嗯,那基本上跟我的理解差不多,Effective Java里面确实也提到了序列化这个问题,就像你说的那样,单例需要注意序列化的问题是一个很严谨的做法,我的疑惑其实一直都是什么情况下在生产实践中需要注意这个问题(我们大多数的代码有静态内部类的实现已经非常够了)。

另外,枚举的用法从我目前接触的代码来看,不太常见,大家貌似还是更喜欢 double check,Kotlin 默认的 object 的 plainOld 也基本上很够用了,是在不行可以用同步的lazy,这个也是 double check 的实现。

谢谢你~

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

这个我认为是 Java 的**所体现出来的,Java 体现出来的最重要**就是 『Babysitting』;

程序员是笨的,是傻的,会毫不犹豫犯错;

所以库的设计和实现就必须考虑到上层调用者的 SB,尽量 fail-fast;

或者直接采用防御性编程,尽量防御掉上层的错误使用。

相比 Python 的『We are considered as adult』来说的确要做比较多的工作。

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

关于 PlainOldSingleton ,实际上还是有差别的;

差别在于,类什么时候会被加载?

即使是饿汉式加载也是单例被其他类 引用 的时候进行加载。

那么这里就有一个问题,就是我们在使用一个 object 的时候,是直接使用类名进行方法调用的;

object Test {
    fun doSomething() { ... }
}

fun main(args: Array<String>) {
    Test.doSomething()
}

也就是说,引用这个单例的同时,我们就已经在使用这个类的实例了;

这样就不会出现所谓的懒加载问题,因为当类加载的时候,你就已经在用这个实例了。

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024

都是类加载的时候实例化,其实没区别的

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

实际上我去 Kotlin 社区发帖,JB 的人跟我说:

Isn't the class get loaded when it first used?

所以查了一下 JVM 规范,然后自己试验了一下,的确是只有当类被主动引用的时候才会被加载;

此时,符合懒加载要求;
懒加载即当你用到的时候才进行类初始化,而你主动引用的时候也就是你使用类实例的时候。

不过知乎上有人提醒我当使用注解扫描的时候,需要通过反射扫描 classpath,会导致类加载过程的初始化。

目前还正在找资料和验证,不知道你的想法如何

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024

public static final Test INSTANCE ;

static {
new Test();
}

或者说 public static final Test INSTANCE = new Test()

这样的代码都是在类加载时执行。

只要触发类加载,就实例化,实际上不是懒加载,所以才是 plainOld 嘛。但那又怎么样呢,除非某些特别复杂的单例初始化耗时,否则直接加载就实例化就行了,不是什么大事儿。

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

只要触发类加载,就实例化,实际上不是懒加载

这个问题我和 JB 的人提过,然后他的反驳是:
「为什么不是呢?你触发类加载的时候不正是你要用这个单例的实例的时候吗?」

他认为懒加载实际上是不存在的,是伪需求。
具体可以到 https://discuss.kotlinlang.org/t/kotlin-singleton-implementation/2853 去围观

from kotlin-tutorials.

wafer-li avatar wafer-li commented on June 28, 2024

JVM 规范中规定了仅有 5 种可以进行类初始化的情况;

JVM 规范关于类加载的准备流程规定你赋给类静态变量的值是在初始化流程进行实例化,准备流程只负责将静态变量设为初值,比如说 0 、null
这里就相当于强制规定了只有 5 种情况可以导致静态变量实例化

根据这 5 种情况,结合 Kotlin 的 object 单例,可以导致单例提前加载的只有反射和调用单例中的其他静态变量;

反射的典型场景是使用 classpath scanner 对注解和其他的一些东西进行扫描;

不过 JB 的人提出,classpath scanner 并不需要使用反射进行扫描;

于是我搞了一个 classpath scanner 测试了一下,结果在这个 gist

使用的 classpath scanner 是 fast-classpath-scanner

main() 方法位于 SingletonTest.kt 中,查一下就可以看到,它不仅打印出了 test.Test 这个单例;

同时,只有 SingletonTestKt 这个 class 被加载了,而 test.Test 这个单例并没有被加载进去。

而对于使用其他静态变量,首先, object 中的 val 虽然是静态变量,但是引用的使用是通过 INSTANCE 引用的;

image

而如果使用 const val,那么编译器就会直接替换为字面量,而不会去加载单例。

image

综上所述,Kotlin object 的单例本身就是懒加载的

from kotlin-tutorials.

bennyhuo avatar bennyhuo commented on June 28, 2024
  1. 懒加载的定义需要给出。通常来讲,用到实例才加载是懒加载,那么类加载的时候就不应该加载(因为我们还没有用到实例),这个意义上来讲,Kotlin 的 object 本质上的实现,不符合 Java 角度对懒加载的定义。
  2. 懒加载是不是需要的很迫切,确实要因情况而定。对于大型服务系统,会比较迫切,小型应用这个是有点儿尴尬的。
  3. 如果非说 Kotlin 的 object 是懒加载的,我想,从 Kotlin 的角度来理解倒也不是什么问题,但这仍然不能改变 Java 虚拟机上 Kotlin object 的实现是 Plain Old Java Singleton 的事实。

from kotlin-tutorials.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.