博客主页

给gitalk添加匿名评论功能

gitalk是一个基于github issue的评论系统,这个想法是非常具有创意的。不过一直以来这个项目都存在一些问题。

  1. 整个OAuth流程放在前端,暴露了client_id和client_secret
  2. github权限粒度较高,用户授权时会把仓库所有的权限授权出去,容易被人恶意利用
  3. 获取access_token是通过第三方api代理获取,有被恶意利用的可能

这几个重要缺陷一直是我的心头刺,因此我Fork了一份代码自己把这两个问题fix了。首先我把授权流程放到了server端,解决第一个和第三个问题。然后我新增了一个匿名评论功能,来打消访客的安全疑虑,同时匿名评论也能增加游客评论欲望。本文着重讲一下怎样实现匿名评论的功能。

Read more

用JS解决Github Pages图片加载慢的问题

GitHub Pages的服务器不在国内,也没有CDN,因此在国内的访问速度本身就不快,如果文章还包含了自己上传到github pages的图片,那加载速度会更惨不忍睹。

对于图片加载慢的问题,唯一的解决办法就是不要把图片上传到github pages,另外寻找图床把图片地址引用到文章中。不过图床有个很大的问题就是稳定性,一旦第三方服务取消支持外链(比如微博2019年启用防盗链,大量图片无法显示),文章将会出现一大堆无法显示的图。

因此最好的办法还是有双管齐下,一份放在图床,另外一份上传到自己的gihub pages 仓库,当图床不可用时,自动替换为我们备份的地址(慢一点总比显示不了强)。

要实现这功能其实并不难,我们都是知道dom的img标签有个onerror事件,当图片加载出错,就会回调这个function。基于这一点,只需要给我们文章的图片都加上onerror事件即可。思路如下:

Read more

谈谈Spring中的InstantiationAwareBeanPostProcessor

熟悉Spring的朋友应该都知道InstantiationAwareBeanPostProcessor这个接口。从它的继承结构可以看出它是一个BeanPostProcessor,不过它是一个非常特殊的BeanPostProcessor,因为它的贯穿了bean创建的每一个周期。

Bean创建流程

/assets/images/bean/spring-iiap-class.png

上面展示了InstantiationAwareBeanPostProcessor3个主要的方法,它们都会在Bean的创建周期中被回调,用以实现拦截或其他的自定义处理。前三个方法分别是实例化之前阶段、实例化后阶段、赋值阶段。因为第四个已经被废弃,这里也不在赘述。

上面提到的三个方法,其实正好是对应了Spring Bean创建周期,对于一个普通的Spring Bean,当它被请求创建时,会经历下面的流程

/assets/images/bean/bean-creation.jpg

Read more

Spring和SpringBoot自动装配原理

“自动装配”这个概念Spring官方提到的不多,仅在SpringBoot中有大概的介绍。根据功能对他总结,可以把自动装配理解为: 通过注解或者特定的配置,能实现自动加载一整个模块的功能,不需要开发者做太多的配置。

在Spring Boot的时代,这功能非常常见,我们所使用的所有Spring Boot Starter都实现了自动装配。不过这个功能并不是Spring Boot的专利,实际上Spring Framework早就实现了这个功能。

要解释Spring的自动装配,先要了解一下Spring配置的历史。

Read more

Java泛型的类型擦除始末,找回被擦除的类型

Java设计之初,语言的规范和虚拟机的规范是分开的。Java语言的规范叫JLS(Java Language Specification),Java虚拟机规范叫JVMS(Java Virtual Machine Specification)。

最初,JLS和JVMS都没有考虑到泛型。泛型在JDK 5开始引入,虽然Java语言支持泛型,不过JVM并没有这个概念,如果需要JVM支持,大量的字节码可能会被重新定义,因为变化实在太大,因此只能在编译时,把类型擦除掉,实现向下兼容。为了更好地理解本文,读者最好准备以下知识:

  • 熟悉泛型的概念

    Generic、Parameterized、Type Parameter、Type Argument等,可在Java泛型几个常见的术语补充相关知识。

  • Java反射
  • JVM简单的字节码

Read more

Java调用wait()和notify()必须获得锁的原因

今天看到个很有意思的问题,《为什么使用 notify 和 notifyAll 的时候要先获得锁》?

这个问题其实真不好回答,就像大家习以为常的事物,突然被问为什么了。这问题我思考了一下,同时也去stackoverflow找到同类型的问题,不过答案都无法令我满意。最后翻到了jls(Java Language Specification),大概知道了真正的原因。

我觉得为什么notify必须要synchorized,根本原因在于对jls规范中规定了对wait set的操作必须是原子的。先看看jls对wait set的描述。

Every object, in addition to having an associated monitor, has an associated wait set. A wait set is a set of threads.

When an object is first created, its wait set is empty. Elementary actions that add threads to and remove threads from wait sets are atomic. Wait sets are manipulated solely through the methods Object.wait, Object.notify, and Object.notifyAll.

Ref: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html

简单来说,当对象创建时,会顺带创建Monitor和Wait Set,这些是在C语言层面去创建的。然后它告诉我们对Wait Set的操作都是原子的,这就能解释,为什么wait和notify必须获得锁。因为没有锁,就没办法保证对wait set的操作是原子的。

Read more

Java什么时候才能用synchronized

编写多线程的代码时,使用synchronized关键字能提供JVM级的线程同步。因为synchronized本身性能不高,于是Doug Lea编写了JUC模块,提供了AQS这样强大的接口,以及ReentrantLock这样方便的类。

也许你会听到有人跟你讲,jdk6后synchronized得到了优化,性能已经不低了。但实际上,在多线程竞争时,synchronized效率依然很低,线程竞争激烈时,还是不可以用synchronized,为什么呢?

上面的图片出自openjdk HotSpot对synchronized的描述,右边是jdk6以前的加锁流程,左边是jdk6后的优化,即加入了偏向锁。简单来说,当第一个线程进入synchronized代码块时,JVM会利用对象头的mark word存下当前线程ID,下次同一线程再进入,只需要对比线程ID即可,不需要经过右边那样的加锁过程。

因此我们可以看到,jdk6优化的是同一线程的进入,但在竞争激烈的情况下,还是要走普通的加锁流程。右边的流程,即是是同一线程进入,也要经过一次CAS操作判断锁的归属,CAS操作变多,带来的是很大的性能损耗。

那么ReentrantLock为什么性能更高呢?我们可以看看它的加锁过程。

final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
    if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
    }
  }
  else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
  }
  return false;
}

上面代码,只有第一次获得锁时才会执行CAS操作,加锁失败会执行park进入队列等待唤醒。else if 判断的是线程重进入,只是对比线程,不需要执行CAS操作。因此,ReentrantLock在资源竞争激烈时,也能保持较高的性能。

回到原本的问题,什么情况下才能用synchronized呢? 很简单,在肯定资源几乎不会有竞争的情况下,才可以使用,比如spring的代码。

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			......
		}
	}

还有一点需要注意,使用synchronized最好创建一个Object作为Lock,不要尝试去锁定某个对象本身。因为一旦访问了对象的hash code,偏向锁会失效。

Object lock = new Object();
synchronized(lock) {
  //do something...
}

//不要这样做
//synchronized(this) {
//}

想要知道这些的所有原因,就必须去读一下openjdk这篇文章了。Synchronization and Object Locking

Read more