Halo博客项目源码学习

Halo博客项目源码学习

Halo1.x停更到1.6版本,2.x使用了reactive架构,想基于halo进行二次开发,1.x有部分前后端未分离内容,2.x架构、设计更复杂,考虑到学习成本,先熟悉1.x的代码。

Halo1.x前台页面使用freemarker作为模板引擎,后台管理界面基于Vue,后端数据访问是SpringData JPA,自实现了缓存,参照springsecurity自实现了认证,代码中大量使用了事件监听、切面、函数式编程。

项目结构

  • annotation,定义了控制接口是否禁用的标记敏感信息的两个注解,对应的处理切面在aspect包中。
  • aspect,只有对应annotation包中的两个切面。
  • cache,自实现的缓存,支持内存、level-db、redis三种实现。
  • config,java配置类包
    attibuteconverter包下是JPA的配置,没怎么用过JPA,不熟悉。
    HaloConfiguration简单注入了ObjectMapper、RestTemplate和自实现缓存的实现类。
    HaloMvcConfiguration配置了springmvc
    HaloRequestMappingHandlerMapping过滤了一些静态资源路径,避免匹配到controller,也可以通过配置统一前缀的方式,并且监听了这些路径的变化。在HalomvcConfig注入。
    SwaggerConfiguration,api文档配置。
  • controller,接口
  • core
    freemarker,自定义了一些freemarker指令。
    其余几个分别是对响应response作了处理、全局异常处理、接口日志切面和springdata page对象的序列化器。
  • event,各种事件
  • exception,各种自定义异常。
  • converter,自定义了字符串转枚举的spring converter。
  • filter,一个记录日志的filter,一个处理跨域的filter。
  • handler,各种对象存储的处理、数据库迁移、主题解析。
  • listener,事件监听器。
  • mail,邮件服务。
  • model,模型对象。
  • repository,spring data repository。
  • security,自实现的认证流程,对每一个请求,在过滤器中将当前用户信息(也可能是Null)设置到线程变量中,请求处理完后清除。主要工作在filter中,拦截一些请求,验证token,判断博客是否初始化过等。
  • service,service层。
  • task,处理过期文章的定时任务。
  • theme,主题处理,包括远程拉取、解压、上传等处理。

启动流程分析

项目启动后,spring容器会发布ApplicationStartEvent,在listener.StartedListener中做了以下几件事。

  1. 数据库迁移,halo在版本升级过程中,数据库结构发生过变化,项目启动后自动完成迁移。
  2. 创建了工作目录、数据备份目录、数据导出目录,工作目录在用户目录下的.halo或者.halo-dev(profile是dev时)。
  3. 初始化主题,默认主题是caicai_anotole,会自动将resources/templates/themes/anotole下的主题文件复制到~/.halo/templates/themes/caicat_anotole下。
  4. 打印一些启动信息
  5. 配置git,远程拉取github主题用到。

在FreemarkerConfigAwareListener完成了以下工作
6. 解析当前主题,设置相关变量到freemarker上下文中,供模板使用。
7. 将博客的一些配置设置到freemarker中,如博客标题、描述等信息。
8. 加载当前用户信息,设置到freemarker上下文。

每次上述信息改变后会发布事件并由此类监听,更新模板上下文。

完成以上工作后就可以等待处理请求了。

请求流程分析

启动项目后,以访问http://localhost:8090/main为例,分析执行流程。
首先进入过滤器中,根据配置的顺序,依次执行的filter是

  1. LogFilter,简单记录日志
  2. CorsFilter,处理请求头,允许跨域。
  3. ContentFilter,拦截/main,doFilterInternal中判断博客是否初始化,否,到ContenAuthenticationFailureHandler中返回重定向到/install。
  4. /install请求同样经过LogFilter,CorsFilter。
  5. 到ContentFilter,放行。
  6. 到ApiAuthenticationFilter,放行。
  7. 到AdminAuthenticationFilter,放行。
  8. 到HaloRequestMappingHandlerMapping,不在黑名单中,返回spring默认查找的handler。
  9. 到MainController中的installation方法,返回index.html视图,请求结束。

请求的大致流程如上,部分请求还有切面、全局异常处理等。

敏感信息的处理

通常手机号、邮箱等信息不会随意发送到前端,用户看到的一般是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项目的应用场景是评论通知

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值