diff --git a/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java index 2fc03ab8f..bc40bb1a2 100644 --- a/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java +++ b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java @@ -27,7 +27,7 @@ public class InMemoryCacheStore extends StringCacheStore { } @Override - void putInternal(String key, CacheWrapper cacheWrapper) { + synchronized void putInternal(String key, CacheWrapper cacheWrapper) { Assert.hasText(key, "Cache key must not be blank"); Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); @@ -38,32 +38,30 @@ public class InMemoryCacheStore extends StringCacheStore { } @Override - Boolean putInternalIfAbsent(String key, CacheWrapper cacheWrapper) { + synchronized Boolean putInternalIfAbsent(String key, CacheWrapper cacheWrapper) { Assert.hasText(key, "Cache key must not be blank"); Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper); - // Put the cache wrapper - CacheWrapper putCacheWrapper = cacheContainer.putIfAbsent(key, cacheWrapper); + // Get the value before + Optional valueOptional = get(key); - if (putCacheWrapper == null) { - putCacheWrapper = cacheWrapper; - } - - boolean isEqual = cacheWrapper.equals(putCacheWrapper); - - if (isEqual) { - log.debug("Put successfully"); - } else { + if (valueOptional.isPresent()) { log.warn("Failed to put the cache, because the key: [{}] has been present already", key); + return false; } - return isEqual; + // Put the cache wrapper + putInternal(key, cacheWrapper); + + log.debug("Put successfully"); + + return true; } @Override - public void delete(String key) { + public synchronized void delete(String key) { Assert.hasText(key, "Cache key must not be blank"); cacheContainer.remove(key); diff --git a/src/main/java/cc/ryanc/halo/cache/lock/CacheLock.java b/src/main/java/cc/ryanc/halo/cache/lock/CacheLock.java new file mode 100644 index 000000000..81f228301 --- /dev/null +++ b/src/main/java/cc/ryanc/halo/cache/lock/CacheLock.java @@ -0,0 +1,70 @@ +package cc.ryanc.halo.cache.lock; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * Cache lock annotation. + * + * @author johnniang + * @date 3/28/19 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface CacheLock { + + /** + * Cache prefix, default is "" + * + * @return cache prefix + */ + @AliasFor("value") + String prefix() default ""; + + /** + * Alias of prefix, default is "" + * + * @return alias of prefix + */ + @AliasFor("prefix") + String value() default ""; + + /** + * Expired time, default is 5. + * + * @return expired time + */ + long expired() default 5; + + /** + * Time unit, default is TimeUnit.SECONDS. + * + * @return time unit + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * Delimiter, default is ':' + * + * @return delimiter + */ + String delimiter() default ":"; + + /** + * Whether delete cache after method invocation. + * + * @return true if delete cache after method invocation; false otherwise + */ + boolean autoDelete() default true; + + /** + * Whether trace the request info. + * + * @return true if trace the request info; false otherwise + */ + boolean traceRequest() default false; +} diff --git a/src/main/java/cc/ryanc/halo/cache/lock/CacheLockInterceptor.java b/src/main/java/cc/ryanc/halo/cache/lock/CacheLockInterceptor.java new file mode 100644 index 000000000..6f095a696 --- /dev/null +++ b/src/main/java/cc/ryanc/halo/cache/lock/CacheLockInterceptor.java @@ -0,0 +1,127 @@ +package cc.ryanc.halo.cache.lock; + +import cc.ryanc.halo.cache.StringCacheStore; +import cc.ryanc.halo.exception.FrequentAccessException; +import cc.ryanc.halo.exception.ServiceException; +import cn.hutool.extra.servlet.ServletUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; + +/** + * Interceptor for cache lock annotation. + * + * @author johnniang + * @date 3/28/19 + */ +@Slf4j +@Aspect +@Configuration +public class CacheLockInterceptor { + + private final static String CACHE_LOCK_PREFOX = "cache_lock_"; + + private final static String CACHE_LOCK_VALUE = "locked"; + + private final StringCacheStore cacheStore; + + private final HttpServletRequest httpServletRequest; + + public CacheLockInterceptor(StringCacheStore cacheStore, + HttpServletRequest httpServletRequest) { + this.cacheStore = cacheStore; + this.httpServletRequest = httpServletRequest; + } + + @Around("@annotation(cc.ryanc.halo.cache.lock.CacheLock)") + public Object interceptCacheLock(ProceedingJoinPoint joinPoint) throws Throwable { + // Get method signature + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + + log.debug("Starting locking: [{}]", methodSignature.toString()); + + // Get cache lock + CacheLock cacheLock = methodSignature.getMethod().getAnnotation(CacheLock.class); + + // Build cache lock key + String cacheLockKey = buildCacheLockKey(cacheLock, joinPoint); + + log.debug("Built lock key: [{}]", cacheLockKey); + + + try { + // Get from cache + Boolean cacheResult = cacheStore.putIfAbsent(cacheLockKey, CACHE_LOCK_VALUE, cacheLock.expired(), cacheLock.timeUnit()); + + if (cacheResult == null) { + throw new ServiceException("Unknown reason of cache " + cacheLockKey).setErrorData(cacheLockKey); + } + + if (!cacheResult) { + throw new FrequentAccessException("Frequent access").setErrorData(cacheLockKey); + } + + // Proceed the method + return joinPoint.proceed(); + } finally { + // Delete the cache + if (cacheLock.autoDelete()) { + cacheStore.delete(cacheLockKey); + log.debug("Deleted the cache lock: [{}]", cacheLock); + } + } + } + + private String buildCacheLockKey(@NonNull CacheLock cacheLock, @NonNull ProceedingJoinPoint joinPoint) { + Assert.notNull(cacheLock, "Cache lock must not be null"); + Assert.notNull(joinPoint, "Proceeding join point must not be null"); + + // Get the method + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + + // Build the cache lock key + StringBuilder cacheKeyBuilder = new StringBuilder(CACHE_LOCK_PREFOX); + + String delimiter = cacheLock.delimiter(); + + if (StringUtils.isNotBlank(cacheLock.prefix())) { + cacheKeyBuilder.append(cacheLock.prefix()); + } + + // Handle cache lock key building + Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations(); + + for (int i = 0; i < parameterAnnotations.length; i++) { + log.debug("Parameter annotation[{}] = {}", i, parameterAnnotations[i]); + + for (int j = 0; j < parameterAnnotations[i].length; j++) { + Annotation annotation = parameterAnnotations[i][j]; + log.debug("Parameter annotation[{}][{}]: {}", i, j, annotation); + if (annotation instanceof CacheParam) { + // Get current argument + Object arg = joinPoint.getArgs()[i]; + log.debug("Cache param args: [{}]", arg); + + // Append to the cache key + cacheKeyBuilder.append(delimiter).append(arg.toString()); + } + } + } + + if (cacheLock.traceRequest()) { + // Append http request info + cacheKeyBuilder.append(delimiter).append(ServletUtil.getClientIP(httpServletRequest)); + } + + return cacheKeyBuilder.toString(); + } +} diff --git a/src/main/java/cc/ryanc/halo/cache/lock/CacheParam.java b/src/main/java/cc/ryanc/halo/cache/lock/CacheParam.java new file mode 100644 index 000000000..ec11697b1 --- /dev/null +++ b/src/main/java/cc/ryanc/halo/cache/lock/CacheParam.java @@ -0,0 +1,17 @@ +package cc.ryanc.halo.cache.lock; + +import java.lang.annotation.*; + +/** + * Cache parameter annotation. + * + * @author johnniang + * @date 3/28/19 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface CacheParam { + +} diff --git a/src/main/java/cc/ryanc/halo/exception/FrequentAccessException.java b/src/main/java/cc/ryanc/halo/exception/FrequentAccessException.java new file mode 100644 index 000000000..0370a636f --- /dev/null +++ b/src/main/java/cc/ryanc/halo/exception/FrequentAccessException.java @@ -0,0 +1,18 @@ +package cc.ryanc.halo.exception; + +/** + * Frequent access exception. + * + * @author johnniang + * @date 3/28/19 + */ +public class FrequentAccessException extends BadRequestException { + + public FrequentAccessException(String message) { + super(message); + } + + public FrequentAccessException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/cc/ryanc/halo/utils/BeanUtils.java b/src/main/java/cc/ryanc/halo/utils/BeanUtils.java index 6e69a2ccf..52babb062 100644 --- a/src/main/java/cc/ryanc/halo/utils/BeanUtils.java +++ b/src/main/java/cc/ryanc/halo/utils/BeanUtils.java @@ -123,7 +123,7 @@ public class BeanUtils { String propertyName = propertyDescriptor.getName(); Object propertyValue = beanWrapper.getPropertyValue(propertyName); - // if propertye value is equal to null, add it to empty name set + // if property value is equal to null, add it to empty name set if (propertyValue == null) { emptyNames.add(propertyName); } diff --git a/src/main/java/cc/ryanc/halo/web/controller/admin/api/AdminController.java b/src/main/java/cc/ryanc/halo/web/controller/admin/api/AdminController.java index 96084a860..15e944890 100644 --- a/src/main/java/cc/ryanc/halo/web/controller/admin/api/AdminController.java +++ b/src/main/java/cc/ryanc/halo/web/controller/admin/api/AdminController.java @@ -1,5 +1,6 @@ package cc.ryanc.halo.web.controller.admin.api; +import cc.ryanc.halo.cache.lock.CacheLock; import cc.ryanc.halo.model.dto.CountOutputDTO; import cc.ryanc.halo.model.dto.UserOutputDTO; import cc.ryanc.halo.model.enums.BlogProperties; @@ -10,7 +11,6 @@ import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import javax.validation.Valid; /** @@ -57,7 +57,8 @@ public class AdminController { } @PostMapping("login") - @ApiOperation("Logins with session") + @ApiOperation("Login with session") + @CacheLock(autoDelete = false, traceRequest = true) public UserOutputDTO login(@Valid @RequestBody LoginParam loginParam, HttpServletRequest request) { return new UserOutputDTO().convertFrom(userService.login(loginParam.getUsername(), loginParam.getPassword(), request.getSession())); }