Complete CacheLock support

pull/137/head
johnniang 2019-03-28 19:52:16 +08:00
parent 8d04bec3b2
commit 11f28f11da
7 changed files with 249 additions and 18 deletions

View File

@ -27,7 +27,7 @@ public class InMemoryCacheStore extends StringCacheStore {
} }
@Override @Override
void putInternal(String key, CacheWrapper<String> cacheWrapper) { synchronized void putInternal(String key, CacheWrapper<String> cacheWrapper) {
Assert.hasText(key, "Cache key must not be blank"); Assert.hasText(key, "Cache key must not be blank");
Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
@ -38,32 +38,30 @@ public class InMemoryCacheStore extends StringCacheStore {
} }
@Override @Override
Boolean putInternalIfAbsent(String key, CacheWrapper<String> cacheWrapper) { synchronized Boolean putInternalIfAbsent(String key, CacheWrapper<String> cacheWrapper) {
Assert.hasText(key, "Cache key must not be blank"); Assert.hasText(key, "Cache key must not be blank");
Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper); log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper);
// Put the cache wrapper // Get the value before
CacheWrapper<String> putCacheWrapper = cacheContainer.putIfAbsent(key, cacheWrapper); Optional<String> valueOptional = get(key);
if (putCacheWrapper == null) { if (valueOptional.isPresent()) {
putCacheWrapper = cacheWrapper;
}
boolean isEqual = cacheWrapper.equals(putCacheWrapper);
if (isEqual) {
log.debug("Put successfully");
} else {
log.warn("Failed to put the cache, because the key: [{}] has been present already", key); 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 @Override
public void delete(String key) { public synchronized void delete(String key) {
Assert.hasText(key, "Cache key must not be blank"); Assert.hasText(key, "Cache key must not be blank");
cacheContainer.remove(key); cacheContainer.remove(key);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -123,7 +123,7 @@ public class BeanUtils {
String propertyName = propertyDescriptor.getName(); String propertyName = propertyDescriptor.getName();
Object propertyValue = beanWrapper.getPropertyValue(propertyName); 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) { if (propertyValue == null) {
emptyNames.add(propertyName); emptyNames.add(propertyName);
} }

View File

@ -1,5 +1,6 @@
package cc.ryanc.halo.web.controller.admin.api; 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.CountOutputDTO;
import cc.ryanc.halo.model.dto.UserOutputDTO; import cc.ryanc.halo.model.dto.UserOutputDTO;
import cc.ryanc.halo.model.enums.BlogProperties; import cc.ryanc.halo.model.enums.BlogProperties;
@ -10,7 +11,6 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid; import javax.validation.Valid;
/** /**
@ -57,7 +57,8 @@ public class AdminController {
} }
@PostMapping("login") @PostMapping("login")
@ApiOperation("Logins with session") @ApiOperation("Login with session")
@CacheLock(autoDelete = false, traceRequest = true)
public UserOutputDTO login(@Valid @RequestBody LoginParam loginParam, HttpServletRequest request) { public UserOutputDTO login(@Valid @RequestBody LoginParam loginParam, HttpServletRequest request) {
return new UserOutputDTO().convertFrom(userService.login(loginParam.getUsername(), loginParam.getPassword(), request.getSession())); return new UserOutputDTO().convertFrom(userService.login(loginParam.getUsername(), loginParam.getPassword(), request.getSession()));
} }