Halo(六)
Spring publish-event 机制
监听者模式包含了一个监听者 Listener 与之对应的事件 Event,还有一个事件发布者 EventPublish。
过程就是 EventPublish 发布一个事件,被监听者捕获到,然后执行事件相应的方法。
1. 发布事件
private final ApplicationEventPublisher eventPublisher;
eventPublisher.publishEvent(new LogEvent(this, name, 0, ld));
2. 事件
public class LogEvent extends ApplicationEvent {
private final LogParam logParam;
public LogEvent(Object source, LogParam logParam) {
//事件源(发布事件的对象)
super(source);
this.logParam = logParam;
}
public LogEvent(Object source, String logKey, LogType logType, String content) {
this(source, new LogParam(logKey, logType, content));
}
public LogParam getLogParam() {
return logParam;
}
}
3. 监听器
@Component
public class LogEventListener {
private final LogService logService;
public LogEventListener(LogService logService) {
this.logService = logService;
}
//将方法标记为应用程序事件侦听器
@EventListener
//异步
@Async
public void onApplicationEvent(LogEvent event) {
//转换成 Entity
Log logToCreate = event.getLogParam().convertTo();
//保存日志
logService.create(logToCreate);
}
}
缓存模块
自定义注解并使用
/**
* 缓存锁注解(在具有该注解的方法上:加锁,执行目标方法,释放锁)
*/
@Target(ElementType.METHOD) //用于描述注解的使用范围
@Retention(RetentionPolicy.RUNTIME) //用于描述注解的生命周期
@Documented //表示该注解是否可以生成到 API 文档中
@Inherited //具备继承性(A被注解了,那么继承了A的B够继承到A中的注解)
public @interface CacheLock {
/** 缓存锁key前缀(默认"")*/
@AliasFor("value")
String prefix() default "";
/** 缓存锁key前缀(默认"")*/
@AliasFor("prefix")
String value() default "";
/** 过期时间 */
long expired() default 5;
/** 时间单位(默认 s)*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/** 分隔符(默认 :)*/
String delimiter() default ":";
/** 方法调用后是否删除缓存 */
boolean autoDelete() default true;
/** 是否跟踪请求信息(将请求IP添加到缓存key上)*/
boolean traceRequest() default false;
}
/**
* 缓存参数注释
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
}
Controller:
@PostMapping("/login")
@CacheLock(autoDelete = false) //登陆操作加锁,防止重复登陆
public AuthToken auth(@RequestBody @Valid LoginParam loginParam) {
return adminService.authenticate(loginParam);
}
用于缓存锁注解的拦截器(AOP代理模式)
@Aspect
@Configuration
public class CacheLockInterceptor {
//缓存锁key前缀:cache_lock_
private final static String CACHE_LOCK_PREFOX = "cache_lock_";
//缓存锁value:locked(被锁定状态)
private final static String CACHE_LOCK_VALUE = "locked";
//缓存池
private final StringCacheStore cacheStore;
public CacheLockInterceptor(StringCacheStore cacheStore) {
this.cacheStore = cacheStore;
}
//具有 @annotation(run.halo.app.cache.lock.CacheLock) 注解的方法都会触发拦截器
@Around("@annotation(run.halo.app.cache.lock.CacheLock)")
public Object interceptCacheLock(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取注解类
CacheLock cacheLock = methodSignature.getMethod().getAnnotation(CacheLock.class);
//建立缓存锁key
String cacheLockKey = buildCacheLockKey(cacheLock, joinPoint);
try {
//放入缓存(key是cacheLockKey,value是locked)
//如果缓存中没有此缓存锁key才会放入(返回true),否则会放入失败(返回false)
Boolean cacheResult = cacheStore.putIfAbsent(cacheLockKey, CACHE_LOCK_VALUE, cacheLock.expired(), cacheLock.timeUnit());
if (!cacheResult) {
throw new FrequentAccessException("访问过于频繁,请稍后再试!").setErrorData(cacheLockKey);
}
//处理被注解的方法,获取返回值
return joinPoint.proceed();
} finally {
if (cacheLock.autoDelete()) {
//注解标注调用方法后删除缓存
cacheStore.delete(cacheLockKey);
}
}
}
/**
* 建立缓存锁key(包括:cache_lock_cacheLock.prefix/Method:arg:IP)
*/
private String buildCacheLockKey(CacheLock cacheLock,ProceedingJoinPoint joinPoint) {
//获取方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//建立缓存锁key
StringBuilder cacheKeyBuilder = new StringBuilder(CACHE_LOCK_PREFOX);
//分隔符(:)
String delimiter = cacheLock.delimiter();
if (StringUtils.isNotBlank(cacheLock.prefix())) {
cacheKeyBuilder.append(cacheLock.prefix());
} else {
cacheKeyBuilder.append(methodSignature.getMethod().toString());
}
//获取方法上的参数注解数组(一个方法可以有多个参数,一个参数可以有多个注解)
Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
for (int j = 0; j < parameterAnnotations[i].length; j++) {
//获取注解
Annotation annotation = parameterAnnotations[i][j];
//如果被 @CacheParam 注解,则获取参数,添加到缓存锁key上
if (annotation instanceof CacheParam) {
//获取参数
Object arg = joinPoint.getArgs()[i];
//将参数添加到缓存key上
cacheKeyBuilder.append(delimiter).append(arg.toString());
}
}
}
if (cacheLock.traceRequest()) {
//添加 Request 客户端 IP
cacheKeyBuilder.append(delimiter).append(ServletUtils.getRequestIp());
}
return cacheKeyBuilder.toString();
}
}
缓存包装器(将缓存数据封装成对象)
@Data
@NoArgsConstructor
@AllArgsConstructor
class CacheWrapper<V> implements Serializable {
/** 缓存数据 */
private V data;
/** 过期时间 */
private Date expireAt;
/** 创造时间 */
private Date createAt;
}
缓存池接口
public interface CacheStore<K, V> {
/** 通过key得到缓存 */
Optional<V> get(@NonNull K key);
/** 放入一个会过期的缓存 */
void put(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit timeUnit);
/** 缓存池中不存在key才放入 */
Boolean putIfAbsent(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit timeUnit);
/** 放入一个不会过期的缓存 */
void put(@NonNull K key, @NonNull V value);
/** 删除缓存 */
void delete(@NonNull K key);
}
缓存池抽象类
public abstract class AbstractCacheStore<K, V> implements CacheStore<K, V> {
/** 通过key获取缓存包装器 */
abstract Optional<CacheWrapper<V>> getInternal(@NonNull K key);
/** 放入缓存包装器 */
abstract void putInternal(@NonNull K key, @NonNull CacheWrapper<V> cacheWrapper);
/** 如果key不存在才放入缓存包装器 */
abstract Boolean putInternalIfAbsent(@NonNull K key, @NonNull CacheWrapper<V> cacheWrapper);
/** 通过key获取value */
@Override
public Optional<V> get(K key) {
return getInternal(key).map(cacheWrapper -> {
//判断是否过期(过期时间不是null,并且在当前时间之前,表示已经过期)
if (cacheWrapper.getExpireAt() != null && cacheWrapper.getExpireAt().before(run.halo.app.utils.DateUtils.now())) {
//删除缓存
delete(key);
return null;
}
return cacheWrapper.getData();
});
}
/** 放入带过期时间的缓存 */
@Override
public void put(K key, V value, long timeout, TimeUnit timeUnit) {
putInternal(key, buildCacheWrapper(value, timeout, timeUnit));
}
/** key不存在才放入缓存 */
@Override
public Boolean putIfAbsent(K key, V value, long timeout, TimeUnit timeUnit) {
return putInternalIfAbsent(key, buildCacheWrapper(value, timeout, timeUnit));
}
/** 放入不过期的缓存 */
@Override
public void put(K key, V value) {
putInternal(key, buildCacheWrapper(value, 0, null));
}
/** 构建缓存包装器 */
@NonNull
private CacheWrapper<V> buildCacheWrapper(@NonNull V value, long timeout, @Nullable TimeUnit timeUnit) {
//过期时间必须>=0
Assert.isTrue(timeout >= 0, "Cache expiration timeout must not be less than 0");
Date now = run.halo.app.utils.DateUtils.now();
Date expireAt = null;
//如果设置了过期时间,则构造过期时间
if (timeout > 0 && timeUnit != null) {
expireAt = DateUtils.add(now, timeout, timeUnit);
}
//构建缓存包装器
CacheWrapper<V> cacheWrapper = new CacheWrapper<>();
cacheWrapper.setCreateAt(now);
cacheWrapper.setExpireAt(expireAt);
cacheWrapper.setData(value);
return cacheWrapper;
}
}
字符串缓存池抽象类(将缓存 Data 数据转换成 Json 字符串)
public abstract class StringCacheStore extends AbstractCacheStore<String, String> {
/** 放入值 */
public <T> void putAny(String key, T value) {
try {
//JsonUtils.objectToJson(value):对象转成Json
put(key, JsonUtils.objectToJson(value));
} catch (JsonProcessingException e) {
throw new ServiceException("Failed to convert " + value + " to json", e);
}
}
public <T> void putAny(@NonNull String key, @NonNull T value, long timeout, @NonNull TimeUnit timeUnit) {
try {
put(key, JsonUtils.objectToJson(value), timeout, timeUnit);
} catch (JsonProcessingException e) {
throw new ServiceException("Failed to convert " + value + " to json", e);
}
}
/** 获取值 */
public <T> Optional<T> getAny(String key, Class<T> type) {
return get(key).map(value -> {
try {
//JsonUtils.jsonToObject(value, type):Json转换成对象
return JsonUtils.jsonToObject(value, type);
} catch (IOException e) {
return null;
}
});
}
}
内存缓存池(字符串缓存池)
public class InMemoryCacheStore extends StringCacheStore {
/** 清理计划周期 */
private final static long PERIOD = 60 * 1000; //一分钟
/** 缓存容器 */
private final static ConcurrentHashMap<String, CacheWrapper<String>> CACHE_CONTAINER = new ConcurrentHashMap<>();
//定时任务
private final Timer timer;
public InMemoryCacheStore() {
//定时清理缓存(1分钟)
timer = new Timer();
//开启定时任务,延迟0,周期一分钟
timer.scheduleAtFixedRate(new CacheExpiryCleaner(), 0, PERIOD);
}
/** 获取值 */
@Override
Optional<CacheWrapper<String>> getInternal(String key) {
//获取value,并放入允许null的Optional对象中
return Optional.ofNullable(CACHE_CONTAINER.get(key));
}
/** 放入值 */
@Override
void putInternal(String key, CacheWrapper<String> cacheWrapper) {
//返回原始值
CacheWrapper<String> oldCacheWrapper = CACHE_CONTAINER.put(key, cacheWrapper);
}
/** 不存在才放入值 */
@Override
Boolean putInternalIfAbsent(String key, CacheWrapper<String> cacheWrapper) {
CacheWrapper<String> stringCacheWrapper = CACHE_CONTAINER.putIfAbsent(key, cacheWrapper);
if(stringCacheWrapper==null)return true;
return false;
}
/** 删除缓存 */
@Override
public void delete(String key) {
CACHE_CONTAINER.remove(key);
}
/** 清空缓存前取消所有定时任务 */
@PreDestroy //销毁Bean之前的操作
public void preDestroy() {
//取消所有定时任务
timer.cancel();
}
/** 缓存超时清理任务 */
private class CacheExpiryCleaner extends TimerTask {
//get()中会判断缓存是否过期,过期会删除缓存,并返回null
@Override
public void run() {
CACHE_CONTAINER.keySet().forEach(key -> {
//类名.this表示类名所代表类的对象
if (!InMemoryCacheStore.this.get(key).isPresent()) {
log.info("删除的过期缓存key:[{}]", key);
}
});
}
}
}
Halo(六)的更多相关文章
- Halo 开源项目学习(六):事件监听机制
基本介绍 Halo 项目中,当用户或博主执行某些操作时,服务器会发布相应的事件,例如博主登录管理员后台时发布 "日志记录" 事件,用户浏览文章时发布 "访问文章" ...
- 使用Docker快速搭建Halo个人博客到阿里云服务器上[附加主题和使用域名访问]
一.前言 小编买了一个服务器也是一直想整个网站,一直在摸索,看了能够快速搭建博客系统的教程.总结了有以下几种方式,大家按照自己喜欢的去搭建: halo wordpress hexo vuepress ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息
MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二 ...
- 【原】AFNetworking源码阅读(六)
[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...
- CRL快速开发框架系列教程六(分布式缓存解决方案)
本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...
- 【微信小程序开发•系列文章六】生命周期和路由
这篇文章理论的知识比较多一些,都是个人观点,描述有失妥当的地方希望读者指出. [微信小程序开发•系列文章一]入门 [微信小程序开发•系列文章二]视图层 [微信小程序开发•系列文章三]数据层 [微信小程 ...
- 我的MYSQL学习心得(六) 函数
我的MYSQL学习心得(六) 函数 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) 数据类 ...
- 我的MYSQL学习心得(十六) 优化
我的MYSQL学习心得(十六) 优化 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) 数据 ...
随机推荐
- 535D Tavas and Malekas
题目大意 给你一个串和m个下标 问你一个长度为n的串每一个下标开始的后缀的前缀都包含给定的串的方案数 分析 对于给定的串求出z数组 对于两个串不重叠的情况就是中间都不包含的数随便填即可 对于重叠的情况 ...
- 【GIS数据格式】ArcInfo Binary Grid Format
最近在修改项目时发现有些提取的坡度数据在参与了下一步计算后会出错,仔细跟了代码之后发现AE生成的坡度数据和其他一些分析后的栅格都被存储为.adf文件.说起惭愧,并不了解这是什么数据,由于下层算法使用G ...
- 导入maven项目pom.xml首行报错missing artifact。。。
解决方法: 在maven地址(如一般默认的地址C:\Users\Administrator\.m2\repository\)找到对应包名 加后缀名为.lastUpdated文件(如:ojdbc14.l ...
- CentOS7.X安装FastDFS-5.10
安装准备 yum install \ vim \ git \ gcc \ gcc-c++ \ wget \ make \ libtool \ automake \ autoconf \ -y \ 安装 ...
- 用notepad++ 打造轻量级Java编译器
http://blog.163.com/jackie_howe/blog/static/19949134720125591752396/ 用notepad++ 打造轻量级Java编译器 2012-06 ...
- 逻辑回归提高阈值对p和r的影响
这里我做了一个实验 也就是随着阈值的增大,precision增加或者不变,recall减少或者不变.
- JSP基础--会话跟踪技术、cookie、session
会话跟踪技术 1 什么是会话跟踪技术 我们需要先了解一下什么是会话!可以把会话理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应.例如你给10086打个电话,你就是客户端,而10 ...
- vuejs基础-常见指令(基本结构、v-cloak、v-text、v-html、v-bind、v-model\v-if、v-show)
Vue之 - 基本的代码结构 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...
- SQL常用语句之数据库的创建、删除以及属性的修改-篇幅1
本篇文章主要总结了SQL Server 语句的使用和一些基础知识,因为目前我也正在学习,所以总结一下. 要使用数据库语句,首先就要知道数据库对象的结构: 通常情况下,如果不会引起混淆,可以直接使用对象 ...
- 解决bug:sprongboot2整合shiro,swagger2页面样式加载不出来问题
问题如题: 解决思路,把shiro拦截去掉之后发现swagger-ui.html页面接在的资源如下: 因此可以推断拦截器拦截了 "/swagger-resources" " ...