Halo博客项目源码学习

看了一些Halo博客的源码,觉得十分规范和优雅,从中学习了不少新的技术、业务的实现方式、代码编写规范等,在此记录一下笔记。

敏感信息的处理

通常手机号、邮箱等信息不会随意发送到前端,用户看到的一般是110****8980这样的形式,比较直接的解决方式是在VO的get方法中不返回真实的信息,在Halo中,定义了@SensitiveConceal注解和处理该注解的切面,可以作为一个参考。

@Aspect
@Component
public class SensitiveConcealAspect {

    @Pointcut("within(run.halo.app.repository..*) "
        + "&& @annotation(run.halo.app.annotation.SensitiveConceal)")
    public void pointCut() {
    }

    private Object sensitiveMask(Object comment) {
        if (comment instanceof BaseComment) {
            ((BaseComment) comment).setEmail("");
            ((BaseComment) comment).setIpAddress("");
        }
        return comment;
    }

    @Around("pointCut()")
    public Object mask(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        if (SecurityContextHolder.getContext().isAuthenticated()) {
            return result;
        }
        if (result instanceof Iterable) {
            ((Iterable<?>) result).forEach(this::sensitiveMask);
        }
        return sensitiveMask(result);
    }
}

缓存的实现

项目中缓存的实现应该是参考了SpringCache,没有SpringCache的复杂度,又能满足项目需求,缓存的实现全部在run.app.halo.cache包下。和SpringCache源码对比了一下,发现了一些问题。
在SpringCache的@Caheable注解中有sync属性,用于避免多线程环境下相同的请求同时去访问缓存,而缓存又刚好未命中,给数据库造成压力。
SpringCache中,对于sync为true的方法进行了特殊处理,如下所示,在org.springframework.cache.interceptor.CacheAspectSupport.execute方法中

// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
	CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
	if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
		Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
		Cache cache = context.getCaches().iterator().next();
		try {
			return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
		}
		catch (Cache.ValueRetrievalException ex) {
			// Directly propagate ThrowableWrapper from the invoker,
			// or potentially also an IllegalArgumentException etc.
			ReflectionUtils.rethrowRuntimeException(ex.getCause());
		}
	}
	else {
		// No caching required, only call the underlying method
		return invokeOperation(invoker);
	}
}

在handleSynchronizedGet方法中会限制线程并发访问缓存,但具体还是依赖于Cache的实现。

  • 基于ConcurrentHashMap的Cache,因为map本就是线程安全的,在ConcurrenMapCache中,Spring对各种操作没有作任何限制。
  • 在RedisCache中,存在一个私有的synchronized修饰的getSynchronized方法。

在Halo中,以下两点让人疑惑

  1. InMemoryCache使用ConcurrentHashMap实现,get方法中使用了可重入锁。
  2. RedisCache没有作任何限制。

参照SpringSecurity的认证实现

SpringSecurity是一个比较重量级的框架,且保持有前后端不分离的风格,提供的功能多且复杂,学习成本也较高。Halo仅需要用到一小部分功能,参照框架自实现了一套认证流程,在security包下。

TransactionalEventListener

这篇博客讲得很不错,TransactionalEventListener使用场景以及实现原理
Halo项目的应用场景是评论通知

Q.E.D.


一切很好,不缺烦恼。