├── .gitignore ├── markdown ├── shooting.md ├── limit.md └── release.md ├── src ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── cachex │ │ │ ├── invoker │ │ │ ├── Invoker.java │ │ │ └── adapter │ │ │ │ ├── JoinPointInvokerAdapter.java │ │ │ │ └── InvocationInvokerAdapter.java │ │ │ ├── ICache.java │ │ │ ├── exception │ │ │ └── CacheXException.java │ │ │ ├── domain │ │ │ ├── Pair.java │ │ │ ├── CacheReadResult.java │ │ │ ├── CacheXMethodHolder.java │ │ │ └── CacheXAnnoHolder.java │ │ │ ├── CacheKey.java │ │ │ ├── Invalid.java │ │ │ ├── utils │ │ │ ├── PreventObjects.java │ │ │ ├── CacheXLogger.java │ │ │ ├── PatternGenerator.java │ │ │ ├── SwitcherUtils.java │ │ │ ├── SpelCalculator.java │ │ │ ├── KeyValueUtils.java │ │ │ ├── ResultUtils.java │ │ │ ├── KeyGenerator.java │ │ │ ├── Addables.java │ │ │ ├── ArgNameGenerator.java │ │ │ └── CacheXInfoContainer.java │ │ │ ├── enums │ │ │ └── Expire.java │ │ │ ├── support │ │ │ ├── cache │ │ │ │ ├── NoOpCache.java │ │ │ │ ├── JdkConcurrentMapCache.java │ │ │ │ ├── RedisHelpers.java │ │ │ │ ├── GuavaCache.java │ │ │ │ ├── HBaseCache.java │ │ │ │ ├── RedisClusterCache.java │ │ │ │ ├── RedisPoolCache.java │ │ │ │ ├── LevelDBCache.java │ │ │ │ ├── EhCache.java │ │ │ │ ├── MemcachedCache.java │ │ │ │ └── TairCache.java │ │ │ └── shooting │ │ │ │ ├── MemoryShootingMXBeanImpl.java │ │ │ │ ├── SqliteShootingMXBeanImpl.java │ │ │ │ ├── H2ShootingMXBeanImpl.java │ │ │ │ ├── MySQLShootingMXBeanImpl.java │ │ │ │ ├── DerbyShootingMXBeanImpl.java │ │ │ │ ├── ZKShootingMXBeanImpl.java │ │ │ │ └── AbstractDBShootingMXBean.java │ │ │ ├── CachedGet.java │ │ │ ├── reader │ │ │ ├── AbstractCacheReader.java │ │ │ ├── SingleCacheReader.java │ │ │ └── MultiCacheReader.java │ │ │ ├── Cached.java │ │ │ ├── core │ │ │ ├── CacheXConfig.java │ │ │ ├── CacheXModule.java │ │ │ └── CacheXCore.java │ │ │ ├── ShootingMXBean.java │ │ │ ├── CacheXAspect.java │ │ │ ├── CacheXProxy.java │ │ │ └── manager │ │ │ └── CacheManager.java │ └── resources │ │ └── sql.yaml └── test │ ├── java │ └── com │ │ └── github │ │ └── cachex │ │ ├── domain │ │ ├── Teacher.java │ │ └── User.java │ │ ├── Utils.java │ │ ├── cases │ │ ├── base │ │ │ └── TestBase.java │ │ ├── GroupShootingTest.java │ │ ├── PreventBreakdownTest.java │ │ ├── SingleTest.java │ │ ├── InnerMapTest.java │ │ ├── ExceptionTest.java │ │ └── MultiTest.java │ │ ├── ObjectTestCase.java │ │ ├── service │ │ ├── impl │ │ │ ├── PreventBreakdownServiceImpl.java │ │ │ ├── InnerMapService.java │ │ │ └── UserServiceImpl.java │ │ └── UserService.java │ │ └── shooting │ │ └── MxBeanTest.java │ └── resources │ ├── logback.xml │ └── spring-context.xml ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target/ 3 | *.iml -------------------------------------------------------------------------------- /markdown/shooting.md: -------------------------------------------------------------------------------- 1 | ## 命中率分组统计 2 | > 详见: `com.github.cachex.core.CacheXConfig.shootingMXBean`属性 && `com.github.cachex.ShootingMXBean`接口及实现 3 | 4 | ### 细节待续... -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/invoker/Invoker.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.invoker; 2 | 3 | /** 4 | * @author jifang.zjf 5 | * @since 2017/6/22 下午4:22. 6 | */ 7 | public interface Invoker { 8 | 9 | Object[] getArgs(); 10 | 11 | Object proceed() throws Throwable; 12 | 13 | Object proceed(Object[] args) throws Throwable; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/domain/Teacher.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | /** 4 | * @author jifang 5 | * @since 16/7/22 下午5:57. 6 | */ 7 | public class Teacher { 8 | 9 | private String name; 10 | 11 | public Teacher() { 12 | } 13 | 14 | public Teacher(String name) { 15 | this.name = name; 16 | } 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | 22 | public void setName(String name) { 23 | this.name = name; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/ICache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author jifang 8 | * @since 2016/11/2 下午4:58. 9 | */ 10 | public interface ICache { 11 | 12 | Object read(String key); 13 | 14 | void write(String key, Object value, long expire); 15 | 16 | Map read(Collection keys); 17 | 18 | void write(Map keyValueMap, long expire); 19 | 20 | void remove(String... keys); 21 | } -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * @author jifang.zjf 7 | * @since 2017/6/14 下午9:14. 8 | */ 9 | public class Utils { 10 | 11 | public static void delay(long millis) { 12 | try { 13 | Thread.sleep(millis); 14 | } catch (InterruptedException e) { 15 | e.printStackTrace(); 16 | } 17 | } 18 | 19 | public static int nextRadom() { 20 | return new Random().nextInt(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/base/TestBase.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases.base; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.test.context.ContextConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | 8 | /** 9 | * @author jifang 10 | * @since 2016/11/30 下午2:10. 11 | */ 12 | @RunWith(SpringJUnit4ClassRunner.class) 13 | @ContextConfiguration(locations = "classpath:spring-context.xml") 14 | public class TestBase { 15 | 16 | @Test 17 | public void base() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/exception/CacheXException.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.exception; 2 | 3 | /** 4 | * @author jifang 5 | * @since 16/7/18 下午4:07. 6 | */ 7 | public class CacheXException extends RuntimeException { 8 | 9 | private static final long serialVersionUID = 6582898095150572577L; 10 | 11 | public CacheXException(String message) { 12 | super(message); 13 | } 14 | 15 | public CacheXException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public CacheXException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/domain/Pair.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | /** 4 | * @author jifang 5 | * @since 2016/11/30 上午10:38. 6 | */ 7 | public class Pair { 8 | 9 | private L left; 10 | 11 | private R right; 12 | 13 | public static Pair of(L left, R right) { 14 | return new Pair<>(left, right); 15 | } 16 | 17 | private Pair(L left, R right) { 18 | this.left = left; 19 | this.right = right; 20 | } 21 | 22 | public L getLeft() { 23 | return left; 24 | } 25 | 26 | public R getRight() { 27 | return right; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/CacheKey.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * @author jifang 7 | * @since 16/7/19 下午6:07. 8 | */ 9 | @Documented 10 | @Target(ElementType.PARAMETER) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface CacheKey { 13 | 14 | /** 15 | * @return use a part of param as a cache key part 16 | */ 17 | String value() default ""; 18 | 19 | /** 20 | * @return used multi model(value has `#i` index) and method return {@code Collection}, 21 | * the {@code field} indicate which of the {@code Collection}'s entity field related with this param 22 | */ 23 | String field() default ""; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/Invalid.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | 4 | import java.lang.annotation.*; 5 | 6 | /** 7 | * @author jifang 8 | * @since 16/7/19 下午4:21. 9 | */ 10 | @Documented 11 | @Target(value = ElementType.METHOD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface Invalid { 14 | 15 | /** 16 | * @return as {@code @Cached} 17 | * @since 0.3 18 | */ 19 | String value() default ""; 20 | 21 | /** 22 | * @return as {@code @Cached} 23 | * @since 0.3 24 | */ 25 | String prefix() default ""; 26 | 27 | /** 28 | * @return as {@code @Cached} 29 | * @since 0.3 30 | */ 31 | String condition() default ""; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/sql.yaml: -------------------------------------------------------------------------------- 1 | select : SELECT 2 | pattern, 3 | hit_count, 4 | require_count, 5 | version 6 | FROM t_hit_rate 7 | WHERE pattern = ? 8 | 9 | select_all : SELECT 10 | pattern, 11 | hit_count, 12 | require_count, 13 | version 14 | FROM t_hit_rate 15 | 16 | update : UPDATE t_hit_rate 17 | SET 18 | version = version + 1, 19 | %s = ? 20 | WHERE pattern = ? AND version = ? 21 | 22 | insert : INSERT INTO t_hit_rate (pattern, %s, version) 23 | VALUES (?, ?, 0) 24 | 25 | delete : DELETE FROM t_hit_rate WHERE pattern = ? 26 | 27 | truncate : TRUNCATE TABLE t_hit_rate -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/PreventObjects.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author jifang.zjf 7 | * @since 2017/7/5 下午5:53. 8 | */ 9 | public class PreventObjects { 10 | 11 | public static Object getPreventObject() { 12 | return PreventObj.INSTANCE; 13 | } 14 | 15 | public static boolean isPrevent(Object object) { 16 | return object == PreventObj.INSTANCE || object instanceof PreventObj; 17 | } 18 | 19 | private static final class PreventObj implements Serializable { 20 | 21 | private static final long serialVersionUID = -1102811488039755703L; 22 | 23 | private static final PreventObj INSTANCE = new PreventObj(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/ObjectTestCase.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import com.github.cachex.domain.User; 4 | import com.github.cachex.utils.SpelCalculator; 5 | import org.junit.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | /** 11 | * @author jifang.zjf 12 | * @since 2017/6/23 上午11:15. 13 | */ 14 | public class ObjectTestCase { 15 | 16 | @Test 17 | public void test() { 18 | List users = Arrays.asList(new User(1, "fq1"), new User(2, "fq2")); 19 | String value = (String) SpelCalculator.calcSpelValueWithContext("#users[#index].name + 'feiqing'", new String[]{"users", "user", "index"}, new Object[]{users, new User(3, "fq3"), 0}, "kong"); 20 | System.out.println(value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/enums/Expire.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.enums; 2 | 3 | /** 4 | * @author jifang 5 | * @since 16/7/15 下午6:04. 6 | */ 7 | public interface Expire { 8 | 9 | int NO = -1; 10 | 11 | int FOREVER = 0; 12 | 13 | int ONE_SEC = 1000; 14 | 15 | int FIVE_SEC = 4 * ONE_SEC; 16 | 17 | int TEN_SEC = 2 * FIVE_SEC; 18 | 19 | int ONE_MIN = 6 * TEN_SEC; 20 | 21 | int FIVE_MIN = 5 * ONE_MIN; 22 | 23 | int TEN_MIN = 2 * FIVE_MIN; 24 | 25 | int HALF_HOUR = 30 * TEN_MIN; 26 | 27 | int ONE_HOUR = 2 * HALF_HOUR; 28 | 29 | int TWO_HOUR = 2 * ONE_HOUR; 30 | 31 | int SIX_HOUR = 3 * TWO_HOUR; 32 | 33 | int TWELVE_HOUR = 2 * SIX_HOUR; 34 | 35 | int ONE_DAY = 2 * TWELVE_HOUR; 36 | 37 | int TWO_DAY = 2 * ONE_DAY; 38 | 39 | int ONE_WEEK = 7 * ONE_DAY; 40 | } -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/domain/CacheReadResult.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * @author jifang 7 | * @since 2016/11/2 下午5:45. 8 | */ 9 | public class CacheReadResult { 10 | 11 | private Map hitKeyMap; 12 | 13 | private Set missKeySet; 14 | 15 | public CacheReadResult() { 16 | } 17 | 18 | public CacheReadResult(Map hitKeyMap, Set missKeySet) { 19 | this.hitKeyMap = hitKeyMap; 20 | this.missKeySet = missKeySet; 21 | } 22 | 23 | public Map getHitKeyMap() { 24 | return hitKeyMap == null ? Collections.emptyMap() : hitKeyMap; 25 | } 26 | 27 | public Set getMissKeySet() { 28 | return missKeySet == null ? Collections.emptySet() : missKeySet; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/GroupShootingTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.cases.base.TestBase; 4 | import com.github.cachex.service.UserService; 5 | import org.junit.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.stream.IntStream; 11 | 12 | /** 13 | * @author jifang.zjf 14 | * @since 2017/7/6 下午2:48. 15 | */ 16 | public class GroupShootingTest extends TestBase { 17 | 18 | @Autowired 19 | private UserService userService; 20 | 21 | @Test 22 | public void test() { 23 | System.out.println(Integer.MAX_VALUE); 24 | List ids = IntStream.range(0, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll); 25 | userService.getUsers(ids, "name", "no"); 26 | userService.getUsers(ids, "name", "no"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/NoOpCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author jifang 11 | * @since 2017/1/10 上午11:56. 12 | */ 13 | public class NoOpCache implements ICache { 14 | 15 | @Override 16 | public Object read(String key) { 17 | return null; 18 | } 19 | 20 | @Override 21 | public void write(String key, Object value, long expire) { 22 | // no op 23 | } 24 | 25 | @Override 26 | public Map read(Collection keys) { 27 | return Collections.emptyMap(); 28 | } 29 | 30 | @Override 31 | public void write(Map keyValueMap, long expire) { 32 | // no op 33 | } 34 | 35 | @Override 36 | public void remove(String... keys) { 37 | // no op 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/domain/CacheXMethodHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | /** 4 | * @author jifang 5 | * @since 2016/11/29 下午10:41. 6 | */ 7 | public class CacheXMethodHolder { 8 | 9 | private Class innerReturnType; 10 | 11 | private Class returnType; 12 | 13 | private boolean collection; 14 | 15 | public CacheXMethodHolder(boolean collection) { 16 | this.collection = collection; 17 | } 18 | 19 | public boolean isCollection() { 20 | return collection; 21 | } 22 | 23 | public Class getReturnType() { 24 | return returnType; 25 | } 26 | 27 | public void setReturnType(Class returnType) { 28 | this.returnType = returnType; 29 | } 30 | 31 | public Class getInnerReturnType() { 32 | return innerReturnType; 33 | } 34 | 35 | public void setInnerReturnType(Class innerReturnType) { 36 | this.innerReturnType = innerReturnType; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/invoker/adapter/JoinPointInvokerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.invoker.adapter; 2 | 3 | import com.github.cachex.invoker.Invoker; 4 | import org.aspectj.lang.ProceedingJoinPoint; 5 | 6 | /** 7 | * @author jifang.zjf 8 | * @since 2017/6/22 下午4:26. 9 | */ 10 | public class JoinPointInvokerAdapter implements Invoker { 11 | 12 | private ProceedingJoinPoint proceedingJoinPoint; 13 | 14 | public JoinPointInvokerAdapter(ProceedingJoinPoint proceedingJoinPoint) { 15 | this.proceedingJoinPoint = proceedingJoinPoint; 16 | } 17 | 18 | @Override 19 | public Object[] getArgs() { 20 | return proceedingJoinPoint.getArgs(); 21 | } 22 | 23 | @Override 24 | public Object proceed() throws Throwable { 25 | return proceedingJoinPoint.proceed(); 26 | } 27 | 28 | @Override 29 | public Object proceed(Object[] args) throws Throwable { 30 | return proceedingJoinPoint.proceed(args); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/invoker/adapter/InvocationInvokerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.invoker.adapter; 2 | 3 | import com.github.cachex.invoker.Invoker; 4 | import org.apache.commons.proxy.Invocation; 5 | 6 | /** 7 | * @author jifang.zjf 8 | * @since 2017/6/26 下午4:04. 9 | */ 10 | public class InvocationInvokerAdapter implements Invoker { 11 | 12 | private Object target; 13 | 14 | private Invocation invocation; 15 | 16 | public InvocationInvokerAdapter(Object target, Invocation invocation) { 17 | this.target = target; 18 | this.invocation = invocation; 19 | } 20 | 21 | @Override 22 | public Object[] getArgs() { 23 | return invocation.getArguments(); 24 | } 25 | 26 | @Override 27 | public Object proceed() throws Throwable { 28 | return invocation.proceed(); 29 | } 30 | 31 | @Override 32 | public Object proceed(Object[] args) throws Throwable { 33 | return invocation.getMethod().invoke(target, args); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/CachedGet.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | 4 | import java.lang.annotation.*; 5 | 6 | /** 7 | * @author jifang.zjf 8 | * @since 2017/6/22 下午2:02. 9 | */ 10 | @Documented 11 | @Target(value = ElementType.METHOD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface CachedGet { 14 | 15 | /** 16 | * @return Specifies the Used cache implementation, 17 | * default the first {@code caches} config in {@code CacheXAspect} 18 | * @since 0.3 19 | */ 20 | String value() default ""; 21 | 22 | /** 23 | * @return Specifies the start keyExp on every key, 24 | * if the {@code Method} have non {@code param}, 25 | * {@code keyExp} is the constant key used by this {@code Method} 26 | * @since 0.3 27 | */ 28 | String prefix() default ""; 29 | 30 | /** 31 | * @return use SpEL, 32 | * when this spel is {@code true}, this {@Code Method} will go through by cache 33 | * @since 0.3 34 | */ 35 | String condition() default ""; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/reader/AbstractCacheReader.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.reader; 2 | 3 | import com.github.cachex.domain.CacheXAnnoHolder; 4 | import com.github.cachex.domain.CacheXMethodHolder; 5 | import com.github.cachex.invoker.Invoker; 6 | import com.github.cachex.utils.CacheXLogger; 7 | 8 | /** 9 | * @author jifang 10 | * @since 2016/11/5 下午3:22. 11 | */ 12 | public abstract class AbstractCacheReader { 13 | 14 | public abstract Object read(CacheXAnnoHolder cacheXAnnoHolder, CacheXMethodHolder cacheXMethodHolder, Invoker invoker, boolean needWrite) throws Throwable; 15 | 16 | Object doLogInvoke(ThrowableSupplier throwableSupplier) throws Throwable { 17 | long start = System.currentTimeMillis(); 18 | try { 19 | return throwableSupplier.get(); 20 | } finally { 21 | CacheXLogger.debug("method invoke total cost [{}] ms", (System.currentTimeMillis() - start)); 22 | } 23 | } 24 | 25 | @FunctionalInterface 26 | protected interface ThrowableSupplier { 27 | T get() throws Throwable; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${pattern} 8 | 9 | 10 | 11 | 12 | ${user.home}/logs/cachex/cachex.log 13 | 14 | ${user.dir}/logs/cachex/cachex.log.%d{yyyy-MM-dd} 15 | 1 16 | 17 | 18 | ${pattern} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/CacheXLogger.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * @author jifang.zjf@alibaba-inc.com (FeiQing) 8 | * @version 1.0 9 | * @since 2018-05-28 18:51:00. 10 | */ 11 | public class CacheXLogger { 12 | 13 | private static Logger logger = LoggerFactory.getLogger("com.github.cachex"); 14 | 15 | public static void debug(String format, Object... arguments) { 16 | if (logger.isDebugEnabled()) { 17 | logger.debug(format, arguments); 18 | } 19 | } 20 | 21 | public static void info(String format, Object... arguments) { 22 | if (logger.isInfoEnabled()) { 23 | logger.info(format, arguments); 24 | } 25 | } 26 | 27 | public static void warn(String format, Object... arguments) { 28 | if (logger.isWarnEnabled()) { 29 | logger.warn(format, arguments); 30 | } 31 | } 32 | 33 | public static void error(String format, Object... arguments) { 34 | logger.error(format, arguments); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/Cached.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import com.github.cachex.enums.Expire; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * @author jifang 9 | * @since 2016/11/2 下午2:22. 10 | */ 11 | @Documented 12 | @Target(value = ElementType.METHOD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Cached { 15 | 16 | /** 17 | * @return Specifies the Used cache implementation, 18 | * default the first {@code caches} config in {@code CacheXAspect} 19 | */ 20 | String value() default ""; 21 | 22 | /** 23 | * @return Specifies the start prefix on every key, 24 | * if the {@code Method} have non {@code param}, 25 | * {@code prefix} is the constant key used by this {@code Method} 26 | */ 27 | String prefix() default ""; 28 | 29 | /** 30 | * @return use SpEL, 31 | * when this spel is {@code true}, this {@code Method} will go through by cache 32 | */ 33 | String condition() default ""; 34 | 35 | /** 36 | * @return expire time, time unit: seconds 37 | */ 38 | int expire() default Expire.FOREVER; 39 | } -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/core/CacheXConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.core; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.ShootingMXBean; 5 | import lombok.Data; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * @author jifang.zjf 11 | * @since 2017/7/5 下午3:46. 12 | */ 13 | @Data 14 | public class CacheXConfig { 15 | 16 | // ICache接口实现 17 | private Map caches; 18 | 19 | // 缓存分组命中率统计 20 | private ShootingMXBean shootingMXBean; 21 | 22 | // 是否开启CacheX(全局开关) 23 | private Switch cachex; 24 | 25 | // 是否开启缓存防击穿 26 | private Switch prevent; 27 | 28 | public boolean isPreventOn() { 29 | return prevent != null && prevent == Switch.ON; 30 | } 31 | 32 | public static CacheXConfig newConfig(Map caches) { 33 | CacheXConfig config = new CacheXConfig(); 34 | config.caches = caches; 35 | config.cachex = Switch.ON; 36 | config.prevent = Switch.OFF; 37 | config.shootingMXBean = null; 38 | 39 | return config; 40 | } 41 | 42 | public enum Switch { 43 | ON, 44 | OFF 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/service/impl/PreventBreakdownServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.service.impl; 2 | 3 | import com.github.cachex.CacheKey; 4 | import com.github.cachex.Cached; 5 | import com.github.cachex.domain.User; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.*; 9 | 10 | /** 11 | * @author jifang.zjf 12 | * @since 2017/7/6 上午11:14. 13 | */ 14 | @Component 15 | public class PreventBreakdownServiceImpl { 16 | 17 | @Cached 18 | public Map getMap(@CacheKey("'id:' + #arg0[#i]") List ids, @CacheKey String name) { 19 | Map map = new HashMap<>(ids.size()); 20 | // 故意不返回第一个 21 | for (int i = 1; i < ids.size(); ++i) { 22 | map.put(ids.get(i), new User(ids.get(i), name + ids.get(i))); 23 | } 24 | 25 | return map; 26 | } 27 | 28 | @Cached 29 | public List getUsers(@CacheKey(value = "'id:' + #arg0[#i]", field = "id") Set ids) { 30 | List u = new ArrayList<>(); 31 | for (int i : ids) { 32 | u.add(new User(i, "name" + i)); 33 | } 34 | 35 | return u; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/PatternGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.CacheKey; 4 | import com.github.cachex.domain.CacheXAnnoHolder; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.Collection; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | 11 | /** 12 | * @author jifang 13 | * @since 2017/3/1 下午5:55. 14 | */ 15 | public class PatternGenerator { 16 | 17 | private static final ConcurrentMap patterns = new ConcurrentHashMap<>(); 18 | 19 | public static String generatePattern(CacheXAnnoHolder cacheXAnnoHolder) { 20 | return patterns.computeIfAbsent(cacheXAnnoHolder.getMethod(), (method) -> doPatternCombiner(cacheXAnnoHolder)); 21 | } 22 | 23 | private static String doPatternCombiner(CacheXAnnoHolder cacheXAnnoHolder) { 24 | StringBuilder sb = new StringBuilder(cacheXAnnoHolder.getPrefix()); 25 | Collection cacheKeys = cacheXAnnoHolder.getCacheKeyMap().values(); 26 | for (CacheKey cacheKey : cacheKeys) { 27 | sb.append(cacheKey.value()); 28 | } 29 | 30 | return sb.toString(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.service; 2 | 3 | 4 | import com.github.cachex.domain.User; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author jifang 12 | * @since 16/7/19 下午6:24. 13 | */ 14 | public interface UserService { 15 | 16 | /** 17 | * multi 18 | ***/ 19 | Map returnMap(String app, List ids, Object noKey); 20 | 21 | void invalidMap(String apps, List ids); 22 | 23 | List getUsers(List ids, String name, Object non); 24 | 25 | void invalidList(List users); 26 | 27 | /*** 28 | * single 29 | ****/ 30 | User singleKey(int id, String name, Object non); 31 | 32 | void singleRemove(int id, String name, Object non); 33 | 34 | void updateUser(User user, String name, Object non); 35 | 36 | boolean spelCompose(User user); 37 | 38 | /** 39 | * ops 40 | */ 41 | void noParam(); 42 | 43 | void noCacheKey(Object o); 44 | 45 | void wrongMultiParam(Object o); 46 | 47 | Map wrongIdentifier(List ids); 48 | 49 | List wrongCollectionReturn(List ids); 50 | 51 | List correctIdentifier(List ids); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/JdkConcurrentMapCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | 11 | /** 12 | * @author jifang 13 | * @since 2017/1/10 上午10:29. 14 | */ 15 | public class JdkConcurrentMapCache implements ICache { 16 | 17 | private ConcurrentMap map = new ConcurrentHashMap<>(); 18 | 19 | @Override 20 | public Object read(String key) { 21 | return map.get(key); 22 | } 23 | 24 | @Override 25 | public void write(String key, Object value, long expire) { 26 | map.put(key, value); 27 | } 28 | 29 | @Override 30 | public Map read(Collection keys) { 31 | Map subCache = new HashMap<>(keys.size()); 32 | for (String key : keys) { 33 | subCache.put(key, read(key)); 34 | } 35 | 36 | return subCache; 37 | } 38 | 39 | @Override 40 | public void write(Map keyValueMap, long expire) { 41 | map.putAll(keyValueMap); 42 | } 43 | 44 | @Override 45 | public void remove(String... keys) { 46 | for (String key : keys) { 47 | map.remove(key); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/RedisHelpers.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.jbox.serializer.ISerializer; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * @author jifang.zjf@alibaba-inc.com (FeiQing) 9 | * @version 1.0 10 | * @since 2018-09-03 14:15:00. 11 | */ 12 | class RedisHelpers { 13 | 14 | /* For Write */ 15 | static byte[][] toByteArray(Map keyValueMap, ISerializer serializer) { 16 | byte[][] kvs = new byte[keyValueMap.size() * 2][]; 17 | int index = 0; 18 | for (Map.Entry entry : keyValueMap.entrySet()) { 19 | kvs[index++] = entry.getKey().getBytes(); 20 | kvs[index++] = serializer.serialize(entry.getValue()); 21 | } 22 | return kvs; 23 | } 24 | 25 | /* For Read */ 26 | static byte[][] toByteArray(Collection keys) { 27 | byte[][] array = new byte[keys.size()][]; 28 | int index = 0; 29 | for (String str : keys) { 30 | array[index++] = str.getBytes(); 31 | } 32 | return array; 33 | } 34 | 35 | static Map toObjectMap(Collection keys, List bytesValues, ISerializer serializer) { 36 | 37 | int index = 0; 38 | Map result = new HashMap<>(keys.size()); 39 | for (String key : keys) { 40 | Object value = serializer.deserialize(bytesValues.get(index++)); 41 | result.put(key, value); 42 | } 43 | 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/PreventBreakdownTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.cases.base.TestBase; 4 | import com.github.cachex.domain.User; 5 | import com.github.cachex.service.impl.PreventBreakdownServiceImpl; 6 | import org.junit.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.IntStream; 15 | 16 | /** 17 | * @author jifang.zjf 18 | * @since 2017/7/6 上午11:12. 19 | */ 20 | public class PreventBreakdownTest extends TestBase { 21 | 22 | 23 | @Autowired 24 | private PreventBreakdownServiceImpl service; 25 | 26 | @Test 27 | public void test() throws InterruptedException { 28 | List ids = IntStream.range(0, 3).boxed().collect(Collectors.toList()); 29 | 30 | Map map = service.getMap(ids, "-feiqing"); 31 | System.out.println("first map.size() = " + map.size()); 32 | 33 | ids.add(4); 34 | map = service.getMap(ids, "-feiqing"); 35 | System.out.println("second map.size() = " + map.size()); 36 | } 37 | 38 | @Test 39 | public void test2() throws InterruptedException { 40 | Set sets = new HashSet<>(); 41 | for (int i = 0; i < 10; ++i) { 42 | sets.add(i); 43 | } 44 | 45 | System.out.println(service.getUsers(sets)); 46 | sets.add(10); 47 | System.out.println(service.getUsers(sets)); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /markdown/limit.md: -------------------------------------------------------------------------------- 1 | # TODO: 目标: 工业级缓存解决方案 2 | - 多级缓存设计&实现(调研中); 3 | - 消除限制5: `@CachedPut`注解(调研中); 4 | - `@Invalid`开启前向清除缓存(调研中); 5 | - 缓存预热(调研中); 6 | 7 | --- 8 | # 开源依赖 9 | jbox项目室cachex的强依赖之一, 但尚未上传的Maven中央仓库, 有需要编译cachex的同学可自行下载jbox并安装到本地. 10 | 项目地址: https://github.com/feiqing/jbox 11 | 12 | --- 13 | # 合作 14 | cachex项目启动以来, 收到了很多同学的反馈以及协助才能走到今天, 15 | CacheX是个开放的产品, 如果有希望加入我们的同学可以邮箱联系我, 一起共建cachex的未来! 16 | > jifang.zjf@alibaba-inc.com 17 | 18 | ---- 19 | # CacheX使用限制 20 | 21 | --- 22 | ### 1. 多个@CacheKey属性为批量模式 23 | ``` 24 | @Cached 25 | Object func(@CacheKey("#arg0[#i]") List fIds, @CacheKey("#arg1[#i]") List aIds); 26 | ``` 27 | > 该模式会导致CacheX对方法参数做**笛卡尔积**, 结果将会计算产生大量的缓存`key`, 性能损耗较大, 因此不支持. 28 | 29 | ### 2. 以参数Map作为批量模式参数 30 | 31 | ``` 32 | @Cached 33 | Object func(@CacheKey("#arg0[#i]") Map map); 34 | ``` 35 | > 同上: 如果将Map内所有的`Key`/`Value`进行交叉拼装为缓存`key`的话, 也会产生类似**笛卡尔积**的效果, 因此也不支持. 36 | 37 | ### 3. 以Map.keySet()作为批量模式参数 38 | ``` 39 | @Cached 40 | Object func(@CacheKey("#arg0.keySet()[#i]") Map map); 41 | ``` 42 | > 这种模式不常用且实现复杂、性能损耗较大, 因此不支持. 43 | 44 | ### 4. 非标准容器作为批量模式参数 45 | ``` 46 | @Cached 47 | Object func(@CacheKey("#arg0[#i]") List ids) { 48 | return Collections.emptyList(); 49 | } 50 | ``` 51 | 52 | > 由于在批量模式下, CacheX会在构造容器返回值时反射调用容器类的默认构造方法, 以及向容器内添加元素, 但这些容器并未暴露这些方法, 因此不能支持. 53 | 54 | - 这类容器有: 55 | - Arrays.ArrayList 56 | - Collections.SingleList 57 | - ... 58 | > 老版本的CacheX曾经支持过一段时间, 但由于在方法返回前需要转换为MutableCollection, 在方法返回时又要转换回去, 性能损耗较大, 因此后面就废掉了. 59 | 60 | ### 5. 缓存更新 61 | 框架现在还只支持添加缓存和失效缓存两种操作, 暂时还不能支持缓存更新(但其实失效后再添加就是更新了O(∩_∩)O~). 62 | 63 | > 我们目标在将来提供`@CachePut`注解, 以提供根据方法的入参/返回值进行缓存写入/更新, 详见#TODO列表 -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/shooting/MxBeanTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.shooting; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | import com.github.cachex.support.shooting.DerbyShootingMXBeanImpl; 5 | import com.github.cachex.support.shooting.H2ShootingMXBeanImpl; 6 | import org.junit.Test; 7 | 8 | import javax.management.*; 9 | import java.lang.management.ManagementFactory; 10 | 11 | /** 12 | * @author jifang.zjf 13 | * @since 2017/6/10 下午1:32. 14 | */ 15 | public class MxBeanTest { 16 | 17 | @Test 18 | public void testDerby() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, InterruptedException { 19 | ShootingMXBean mxBean = new DerbyShootingMXBeanImpl(); 20 | 21 | ManagementFactory.getPlatformMBeanServer().registerMBean(mxBean, new ObjectName("com.github.cacherx:name=hit")); 22 | mxBean.hitIncr("nihao", 1); 23 | mxBean.reqIncr("nihao", 2); 24 | 25 | // Thread.sleep(1000000); 26 | //mxBean.reset("nihao"); 27 | 28 | //mxBean.reqIncr("testDerby", 88); 29 | //mxBean.resetAll(); 30 | } 31 | 32 | @Test 33 | public void testH2() throws InterruptedException, MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException { 34 | ShootingMXBean mxBean = new H2ShootingMXBeanImpl(); 35 | 36 | ManagementFactory.getPlatformMBeanServer().registerMBean(mxBean, new ObjectName("com.github.cacherx:name=shooting")); 37 | mxBean.hitIncr("nihao", 1); 38 | mxBean.reqIncr("nihao", 2); 39 | 40 | // Thread.sleep(1000000); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/GuavaCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.google.common.cache.CacheBuilder; 5 | import com.google.common.cache.CacheLoader; 6 | import com.google.common.cache.LoadingCache; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.Map; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * @author jifang 15 | * @since 2016/11/2 下午5:03. 16 | */ 17 | public class GuavaCache implements ICache { 18 | 19 | private LoadingCache guavaCache; 20 | 21 | public GuavaCache(long size, long expire) { 22 | guavaCache = CacheBuilder 23 | .newBuilder() 24 | .maximumSize(size) 25 | .expireAfterWrite(expire, TimeUnit.MILLISECONDS) 26 | .build(new CacheLoader() { 27 | @Override 28 | public Object load(String key) { 29 | return null; 30 | } 31 | }); 32 | } 33 | 34 | @Override 35 | public Object read(String key) { 36 | return guavaCache.getIfPresent(key); 37 | } 38 | 39 | @Override 40 | public Map read(Collection keys) { 41 | return guavaCache.getAllPresent(keys); 42 | } 43 | 44 | @Override 45 | public void write(String key, Object value, long expire) { 46 | guavaCache.put(key, value); 47 | } 48 | 49 | @Override 50 | public void write(Map keyValueMap, long expire) { 51 | guavaCache.putAll(keyValueMap); 52 | } 53 | 54 | @Override 55 | public void remove(String... keys) { 56 | guavaCache.invalidateAll(Arrays.asList(keys)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/ShootingMXBean.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author jifang 7 | * @since 2017/3/2 上午11:48. 8 | */ 9 | public interface ShootingMXBean { 10 | 11 | void reqIncr(String pattern, int count); 12 | 13 | void hitIncr(String pattern, int count); 14 | 15 | Map getShooting(); 16 | 17 | void reset(String pattern); 18 | 19 | void resetAll(); 20 | 21 | class ShootingDO { 22 | 23 | private long hit; 24 | 25 | private long required; 26 | 27 | private String rate; 28 | 29 | public static ShootingDO newInstance(long hit, long required) { 30 | double rate = (required == 0 ? 0.0 : hit * 100.0 / required); 31 | String rateStr = String.format("%.1f%s", rate, "%"); 32 | 33 | return new ShootingDO(hit, required, rateStr); 34 | } 35 | 36 | public static ShootingDO mergeShootingDO(ShootingDO do1, ShootingDO do2) { 37 | long hit = do1.getHit() + do2.getHit(); 38 | long required = do1.getRequired() + do2.getRequired(); 39 | 40 | return newInstance(hit, required); 41 | } 42 | 43 | private ShootingDO(long hit, long required, String rate) { 44 | this.hit = hit; 45 | this.required = required; 46 | this.rate = rate; 47 | } 48 | 49 | public long getHit() { 50 | return hit; 51 | } 52 | 53 | public long getRequired() { 54 | return required; 55 | } 56 | 57 | public String getRate() { 58 | return rate; 59 | } 60 | } 61 | 62 | default String summaryName() { 63 | return "zh".equalsIgnoreCase(System.getProperty("user.language")) ? "全局" : "summary"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/SwitcherUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.Cached; 4 | import com.github.cachex.CachedGet; 5 | import com.github.cachex.Invalid; 6 | import com.github.cachex.core.CacheXConfig; 7 | import com.github.cachex.enums.Expire; 8 | 9 | import java.lang.reflect.Method; 10 | 11 | /** 12 | * @author jifang 13 | * @since 2017/1/9 下午4:59. 14 | */ 15 | public class SwitcherUtils { 16 | 17 | public static boolean isSwitchOn(CacheXConfig config, Cached cached, Method method, Object[] args) { 18 | return doIsSwitchOn(config.getCachex() == CacheXConfig.Switch.ON, 19 | cached.expire(), cached.condition(), 20 | method, args); 21 | } 22 | 23 | 24 | public static boolean isSwitchOn(CacheXConfig config, Invalid invalid, Method method, Object[] args) { 25 | return doIsSwitchOn(config.getCachex() == CacheXConfig.Switch.ON, 26 | Expire.FOREVER, invalid.condition(), 27 | method, args); 28 | } 29 | 30 | public static boolean isSwitchOn(CacheXConfig config, CachedGet cachedGet, Method method, Object[] args) { 31 | return doIsSwitchOn(config.getCachex() == CacheXConfig.Switch.ON, 32 | Expire.FOREVER, cachedGet.condition(), 33 | method, args); 34 | } 35 | 36 | private static boolean doIsSwitchOn(boolean openStat, 37 | int expire, 38 | String condition, Method method, Object[] args) { 39 | if (!openStat) { 40 | return false; 41 | } 42 | 43 | if (expire == Expire.NO) { 44 | return false; 45 | } 46 | 47 | return (boolean) SpelCalculator.calcSpelValueWithContext(condition, ArgNameGenerator.getArgNames(method), args, true); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/HBaseCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.jbox.hbase.HBaseBatis; 5 | import com.github.jbox.hbase.HBaseMode; 6 | import org.springframework.util.CollectionUtils; 7 | 8 | import java.util.*; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * @author jifang.zjf@alibaba-inc.com (FeiQing) 14 | * @version 1.0 15 | * @since 2018-08-13 15:10:00. 16 | */ 17 | @SuppressWarnings("unchecked") 18 | public class HBaseCache implements ICache { 19 | 20 | private HBaseBatis hBaseBatis; 21 | 22 | public HBaseCache(HBaseBatis hBaseBatis) { 23 | this.hBaseBatis = hBaseBatis; 24 | } 25 | 26 | @Override 27 | public Object read(String key) { 28 | return hBaseBatis.get(key); 29 | } 30 | 31 | @Override 32 | public void write(String key, Object value, long expire) { 33 | T model = (T) value; 34 | model.setRowKey(key); 35 | hBaseBatis.put(model); 36 | } 37 | 38 | @Override 39 | public Map read(Collection keys) { 40 | List results = hBaseBatis.gets(new ArrayList<>(keys)); 41 | return results.stream().collect(Collectors.toMap(T::getRowKey, Function.identity())); 42 | } 43 | 44 | @Override 45 | public void write(Map keyValueMap, long expire) { 46 | List models = keyValueMap.entrySet().stream().map(entry -> { 47 | T model = (T) entry.getValue(); 48 | model.setRowKey(entry.getKey()); 49 | 50 | return model; 51 | }).collect(Collectors.toList()); 52 | 53 | hBaseBatis.puts(models); 54 | } 55 | 56 | @Override 57 | public void remove(String... keys) { 58 | hBaseBatis.deletes(Arrays.asList(keys)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/SingleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.cases.base.TestBase; 4 | import com.github.cachex.domain.User; 5 | import com.github.cachex.service.UserService; 6 | import org.junit.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import java.util.Date; 10 | 11 | /** 12 | * @author jifang 13 | * @since 16/7/20 上午10:51. 14 | */ 15 | public class SingleTest extends TestBase { 16 | 17 | @Autowired 18 | private UserService userService; 19 | 20 | @Test 21 | public void testSingleKey() throws InterruptedException { 22 | int id = 1; 23 | System.out.println("#########---"); 24 | userService.singleKey(id, "ff-no-key", "no-key"); 25 | 26 | System.out.println("---"); 27 | userService.singleKey(id, "ff-no-key", "ls"); 28 | 29 | System.out.println("---"); 30 | userService.singleRemove(id, "woca", "ls"); 31 | 32 | System.out.println("---"); 33 | userService.singleKey(id, "ff-no-key", "ls"); 34 | System.out.println("#########---"); 35 | 36 | } 37 | 38 | @Test 39 | public void testRemoveSingleKey() { 40 | int id = 1; 41 | String name = "fq"; 42 | System.out.println("#########---"); 43 | User user = userService.singleKey(id, "ff-no-key", "no-key"); 44 | 45 | System.out.println("---"); 46 | userService.singleRemove(id, name, "not`non"); 47 | 48 | System.out.println("---"); 49 | user = userService.singleKey(id, "ff-no-key", "ls"); 50 | 51 | System.out.println("---"); 52 | user = userService.singleKey(id, "ff-no-key", "ls"); 53 | System.out.println("#########---"); 54 | } 55 | 56 | @Test 57 | public void testSpEL() { 58 | User user = new User(1, "feiqing", new Date(), 1, "hangz"); 59 | userService.spelCompose(user); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/SpelCalculator.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.base.Strings; 5 | import org.springframework.expression.EvaluationContext; 6 | import org.springframework.expression.ExpressionParser; 7 | import org.springframework.expression.spel.standard.SpelExpressionParser; 8 | import org.springframework.expression.spel.support.StandardEvaluationContext; 9 | 10 | /** 11 | * Spel表达式的计算功能(@Cached内的condition、@CacheKey内的spel只是作为一个增值服务, 并不作为核心功能, 只是作为key拼装的一个亮点, 并不是必须功能) 12 | * 13 | * @author jifang.zjf 14 | * @since 2017/6/23 上午10:10. 15 | */ 16 | public class SpelCalculator { 17 | 18 | private static final ExpressionParser parser = new SpelExpressionParser(); 19 | 20 | public static Object calcSpelValueWithContext(String spel, String[] argNames, Object[] argValues, Object defaultValue) { 21 | if (Strings.isNullOrEmpty(spel)) { 22 | return defaultValue; 23 | } 24 | 25 | // 将[参数名->参数值]导入spel环境 26 | EvaluationContext context = new StandardEvaluationContext(); 27 | 28 | Preconditions.checkState(argNames.length == argValues.length); 29 | for (int i = 0; i < argValues.length; ++i) { 30 | context.setVariable(argNames[i], argValues[i]); 31 | } 32 | 33 | // todo: 先将xArg放到这儿, 后面可以再想下可以放到哪儿? 34 | String[] xArgNames = ArgNameGenerator.getXArgNames(argValues.length); 35 | for (int i = 0; i < argValues.length; ++i) { 36 | context.setVariable(xArgNames[i], argValues[i]); 37 | } 38 | 39 | return parser.parseExpression(spel).getValue(context); 40 | } 41 | 42 | public static Object calcSpelWithNoContext(String spel, Object defaultValue) { 43 | if (Strings.isNullOrEmpty(spel)) { 44 | return defaultValue; 45 | } 46 | 47 | return parser.parseExpression(spel).getValue(defaultValue); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/InnerMapTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.Utils; 4 | import com.github.cachex.cases.base.TestBase; 5 | import com.github.cachex.exception.CacheXException; 6 | import com.github.cachex.service.impl.InnerMapService; 7 | import com.google.common.collect.Lists; 8 | import org.junit.Test; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.List; 12 | 13 | /** 14 | * @author jifang.zjf 15 | * @since 2017/6/21 下午7:34. 16 | */ 17 | public class InnerMapTest extends TestBase { 18 | 19 | @Resource 20 | private InnerMapService service; 21 | 22 | @Test(expected = CacheXException.class) 23 | public void unmodifiableMap() { 24 | List list = Lists.newArrayList(1, 2, 3); 25 | 26 | service.unmodifiableMap(list); 27 | service.unmodifiableMap(list); 28 | list.add(Utils.nextRadom()); 29 | service.unmodifiableMap(list); 30 | } 31 | 32 | @Test(expected = CacheXException.class) 33 | public void synchronizedMap() { 34 | List list = Lists.newArrayList(1, 2, 3); 35 | 36 | service.synchronizedMap(list); 37 | service.synchronizedMap(list); 38 | list.add(Utils.nextRadom()); 39 | service.synchronizedMap(list); 40 | } 41 | 42 | @Test(expected = CacheXException.class) 43 | public void checkedMap() { 44 | List list = Lists.newArrayList(1, 2, 3); 45 | 46 | service.checkedMap(list); 47 | service.checkedMap(list); 48 | list.add(Utils.nextRadom()); 49 | service.checkedMap(list); 50 | } 51 | 52 | 53 | @Test(expected = CacheXException.class) 54 | public void immutableMap() { 55 | List list = Lists.newArrayList(1, 2, 3); 56 | 57 | service.immutableMap(list); 58 | service.immutableMap(list); 59 | list.add(Utils.nextRadom()); 60 | service.immutableMap(list); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/ExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.cases.base.TestBase; 4 | import com.github.cachex.domain.User; 5 | import com.github.cachex.exception.CacheXException; 6 | import com.github.cachex.service.UserService; 7 | import com.google.common.collect.Lists; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | /** 16 | * @author jifang 17 | * @since 2016/11/30 下午2:20. 18 | */ 19 | public class ExceptionTest extends TestBase { 20 | 21 | @Autowired 22 | private UserService service; 23 | 24 | @Test 25 | public void test1() { 26 | service.noParam(); 27 | } 28 | 29 | @Test 30 | public void test2() { 31 | service.noCacheKey(new Object()); 32 | } 33 | 34 | @Test(expected = CacheXException.class) 35 | public void test3() { 36 | service.wrongMultiParam(new Object()); 37 | } 38 | 39 | @Test 40 | public void test4() { 41 | service.wrongIdentifier(Lists.newArrayList(1, 2)); 42 | } 43 | 44 | @Test(expected = CacheXException.class) 45 | public void test41() { 46 | service.wrongCollectionReturn(Lists.newArrayList(1, 2)); 47 | } 48 | 49 | @Test(expected = NullPointerException.class) 50 | public void test50() { 51 | List users = service.correctIdentifier(null); 52 | } 53 | 54 | @Test 55 | public void test51() { 56 | service.correctIdentifier(Collections.emptyList()); 57 | service.correctIdentifier(Collections.emptyList()); 58 | } 59 | 60 | @Test 61 | public void test5() { 62 | service.correctIdentifier(Lists.newArrayList(1, 2)); 63 | List users = service.correctIdentifier(Lists.newArrayList(1, 2, 3, 4, 5, 6)); 64 | System.out.println(users); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | /** 7 | * @author jifang. 8 | * @since 2016/5/26 11:16. 9 | */ 10 | public class User implements Serializable { 11 | 12 | private static final long serialVersionUID = -1843332169074089984L; 13 | 14 | private int id; 15 | 16 | private String name; 17 | 18 | private Date birthday; 19 | 20 | private int sex; 21 | 22 | private String address; 23 | 24 | private Teacher teacher; 25 | 26 | public User() { 27 | } 28 | 29 | public User(int id, String name) { 30 | this.id = id; 31 | this.name = name; 32 | } 33 | 34 | public User(int id, String name, Date birthday, int sex, String address) { 35 | this.id = id; 36 | this.name = name; 37 | this.birthday = birthday; 38 | this.sex = sex; 39 | this.address = address; 40 | } 41 | 42 | public int getId() { 43 | return id; 44 | } 45 | 46 | public void setId(int id) { 47 | this.id = id; 48 | } 49 | 50 | public String getName() { 51 | return name; 52 | } 53 | 54 | public void setName(String name) { 55 | this.name = name; 56 | } 57 | 58 | public Date getBirthday() { 59 | return birthday; 60 | } 61 | 62 | public void setBirthday(Date birthday) { 63 | this.birthday = birthday; 64 | } 65 | 66 | public int getSex() { 67 | return sex; 68 | } 69 | 70 | public void setSex(int sex) { 71 | this.sex = sex; 72 | } 73 | 74 | public String getAddress() { 75 | return address; 76 | } 77 | 78 | public void setAddress(String address) { 79 | this.address = address; 80 | } 81 | 82 | public Teacher getTeacher() { 83 | return teacher; 84 | } 85 | 86 | public void setTeacher(Teacher teacher) { 87 | this.teacher = teacher; 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/MemoryShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | 11 | /** 12 | * @author jifang 13 | * @since 2017/3/2 下午2:28. 14 | */ 15 | public class MemoryShootingMXBeanImpl implements ShootingMXBean { 16 | 17 | private ConcurrentMap hitMap = new ConcurrentHashMap<>(); 18 | 19 | private ConcurrentMap requireMap = new ConcurrentHashMap<>(); 20 | 21 | @Override 22 | public void hitIncr(String pattern, int count) { 23 | hitMap.computeIfAbsent( 24 | pattern, 25 | (k) -> new AtomicLong() 26 | ).addAndGet(count); 27 | } 28 | 29 | @Override 30 | public void reqIncr(String pattern, int count) { 31 | requireMap.computeIfAbsent( 32 | pattern, 33 | (k) -> new AtomicLong() 34 | ).addAndGet(count); 35 | } 36 | 37 | @Override 38 | public Map getShooting() { 39 | Map result = new LinkedHashMap<>(); 40 | 41 | AtomicLong statisticsHit = new AtomicLong(0); 42 | AtomicLong statisticsRequired = new AtomicLong(0); 43 | requireMap.forEach((pattern, count) -> { 44 | long hit = hitMap.computeIfAbsent(pattern, (key) -> new AtomicLong(0)).get(); 45 | long require = count.get(); 46 | 47 | statisticsHit.addAndGet(hit); 48 | statisticsRequired.addAndGet(require); 49 | 50 | result.put(pattern, ShootingDO.newInstance(hit, require)); 51 | }); 52 | 53 | result.put(summaryName(), ShootingDO.newInstance(statisticsHit.get(), statisticsRequired.get())); 54 | 55 | return result; 56 | } 57 | 58 | @Override 59 | public void reset(String pattern) { 60 | hitMap.remove(pattern); 61 | requireMap.remove(pattern); 62 | } 63 | 64 | @Override 65 | public void resetAll() { 66 | hitMap.clear(); 67 | requireMap.clear(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/KeyValueUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.core.CacheXConfig; 4 | import com.google.common.base.Strings; 5 | 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | /** 12 | * @author jifang 13 | * @since 2016/11/18 下午3:21. 14 | */ 15 | @SuppressWarnings("unchecked") 16 | public class KeyValueUtils { 17 | 18 | public static Map mapToKeyValue(Map proceedEntryValueMap, 19 | Set missKeys, 20 | Map multiEntry2Key, 21 | CacheXConfig.Switch prevent) { 22 | Map keyValueMap = new HashMap<>(proceedEntryValueMap.size()); 23 | proceedEntryValueMap.forEach((multiArgEntry, value) -> { 24 | String key = multiEntry2Key.get(multiArgEntry); 25 | if (Strings.isNullOrEmpty(key)) { 26 | return; 27 | } 28 | 29 | missKeys.remove(key); 30 | keyValueMap.put(key, value); 31 | }); 32 | 33 | // 触发防击穿逻辑 34 | if (prevent == CacheXConfig.Switch.ON && !missKeys.isEmpty()) { 35 | missKeys.forEach(key -> keyValueMap.put(key, PreventObjects.getPreventObject())); 36 | } 37 | 38 | return keyValueMap; 39 | } 40 | 41 | public static Map collectionToKeyValue(Collection proceedCollection, String idSpel, Set missKeys, Map id2Key, CacheXConfig.Switch prevent) { 42 | Map keyValueMap = new HashMap<>(proceedCollection.size()); 43 | 44 | for (Object value : proceedCollection) { 45 | Object id = SpelCalculator.calcSpelWithNoContext(idSpel, value); 46 | String key = id2Key.get(id); 47 | 48 | if (!Strings.isNullOrEmpty(key)) { 49 | missKeys.remove(key); 50 | keyValueMap.put(key, value); 51 | } 52 | } 53 | 54 | if (prevent == CacheXConfig.Switch.ON && !missKeys.isEmpty()) { 55 | missKeys.forEach(key -> keyValueMap.put(key, PreventObjects.getPreventObject())); 56 | } 57 | 58 | return keyValueMap; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/service/impl/InnerMapService.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.service.impl; 2 | 3 | import com.github.cachex.CacheKey; 4 | import com.github.cachex.Cached; 5 | import com.github.cachex.domain.User; 6 | import com.github.cachex.enums.Expire; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * @author jifang.zjf 13 | * @since 2017/6/21 下午7:25. 14 | */ 15 | @Component 16 | public class InnerMapService { 17 | 18 | @Cached(expire = Expire.TEN_MIN) 19 | public Map unmodifiableMap(@CacheKey(value = "'id:' + #arg0[#i]") List ids) { 20 | if (ids.size() == 1) { 21 | int id = ids.get(0); 22 | return Collections.singletonMap(id, new User(id, "name" + id, new Date(), id, "")); 23 | } else { 24 | Map map = new HashMap<>(); 25 | for (Integer id : ids) { 26 | map.put(id, new User(id, "name" + id, new Date(), id, "")); 27 | } 28 | 29 | return Collections.unmodifiableMap(map); 30 | } 31 | } 32 | 33 | @Cached(expire = Expire.TEN_MIN) 34 | public Map synchronizedMap(@CacheKey(value = "'id:' + #arg0[#i]") List ids) { 35 | Map map = new HashMap<>(); 36 | for (Integer id : ids) { 37 | map.put(id, new User(id, "name" + id, new Date(), id, "")); 38 | } 39 | 40 | return Collections.synchronizedMap(map); 41 | } 42 | 43 | @Cached(expire = Expire.TEN_MIN) 44 | public Map checkedMap(@CacheKey(value = "'id:' + #arg0[#i]") List ids) { 45 | TreeMap map = new TreeMap<>(); 46 | for (Integer id : ids) { 47 | map.put(id, new User(id, "name" + id, new Date(), id, "")); 48 | } 49 | 50 | return Collections.checkedNavigableMap(map, Integer.class, User.class); 51 | } 52 | 53 | @Cached(prefix = "map-", expire = Expire.TEN_MIN) 54 | public Map immutableMap(@CacheKey(value = "'id:' + #arg0[#i]") List ids) { 55 | TreeMap map = new TreeMap<>(); 56 | for (Integer id : ids) { 57 | map.put(id, new User(id, "name" + id, new Date(), id, "")); 58 | } 59 | 60 | return Collections.unmodifiableMap(map); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/CacheXAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import com.github.cachex.core.CacheXConfig; 4 | import com.github.cachex.core.CacheXCore; 5 | import com.github.cachex.core.CacheXModule; 6 | import com.github.cachex.invoker.adapter.JoinPointInvokerAdapter; 7 | import org.aspectj.lang.JoinPoint; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.annotation.After; 10 | import org.aspectj.lang.annotation.Around; 11 | import org.aspectj.lang.annotation.Aspect; 12 | import org.aspectj.lang.reflect.MethodSignature; 13 | 14 | import java.lang.reflect.Method; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author jifang 19 | * @since 2016/11/2 下午2:34. 20 | */ 21 | @Aspect 22 | public class CacheXAspect { 23 | 24 | private CacheXCore core; 25 | 26 | public CacheXAspect(Map caches) { 27 | this(CacheXConfig.newConfig(caches)); 28 | } 29 | 30 | public CacheXAspect(CacheXConfig config) { 31 | core = CacheXModule.coreInstance(config); 32 | } 33 | 34 | @Around("@annotation(com.github.cachex.CachedGet)") 35 | public Object read(ProceedingJoinPoint pjp) throws Throwable { 36 | Method method = getMethod(pjp); 37 | CachedGet cachedGet = method.getAnnotation(CachedGet.class); 38 | return core.read(cachedGet, method, new JoinPointInvokerAdapter(pjp)); 39 | } 40 | 41 | @Around("@annotation(com.github.cachex.Cached)") 42 | public Object readWrite(ProceedingJoinPoint pjp) throws Throwable { 43 | Method method = getMethod(pjp); 44 | Cached cached = method.getAnnotation(Cached.class); 45 | 46 | return core.readWrite(cached, method, new JoinPointInvokerAdapter(pjp)); 47 | } 48 | 49 | @After("@annotation(com.github.cachex.Invalid)") 50 | public void remove(JoinPoint pjp) throws Throwable { 51 | Method method = getMethod(pjp); 52 | Invalid invalid = method.getAnnotation(Invalid.class); 53 | core.remove(invalid, method, pjp.getArgs()); 54 | } 55 | 56 | private Method getMethod(JoinPoint pjp) throws NoSuchMethodException { 57 | MethodSignature ms = (MethodSignature) pjp.getSignature(); 58 | Method method = ms.getMethod(); 59 | if (method.getDeclaringClass().isInterface()) { 60 | method = pjp.getTarget().getClass().getDeclaredMethod(ms.getName(), method.getParameterTypes()); 61 | } 62 | 63 | return method; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/SqliteShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import com.google.common.base.StandardSystemProperty; 4 | import org.springframework.jdbc.core.JdbcOperations; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.function.Supplier; 12 | import java.util.stream.Stream; 13 | 14 | /** 15 | * @author jifang.zjf 16 | * @since 2017/7/10 下午5:52. 17 | */ 18 | public class SqliteShootingMXBeanImpl extends AbstractDBShootingMXBean { 19 | 20 | public SqliteShootingMXBeanImpl() { 21 | this(StandardSystemProperty.USER_HOME.value() + "/.sqlite.db"); 22 | } 23 | 24 | public SqliteShootingMXBeanImpl(String dbPath) { 25 | super(dbPath, Collections.emptyMap()); 26 | } 27 | 28 | @Override 29 | protected Supplier jdbcOperationsSupplier(String dbPath, Map context) { 30 | return () -> { 31 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 32 | dataSource.setDriverClassName("org.sqlite.JDBC"); 33 | dataSource.setUrl(String.format("jdbc:sqlite:%s", dbPath)); 34 | 35 | JdbcTemplate template = new JdbcTemplate(dataSource); 36 | template.execute("CREATE TABLE IF NOT EXISTS t_hit_rate(" + 37 | "id BIGINT IDENTITY PRIMARY KEY," + 38 | "pattern VARCHAR(64) NOT NULL UNIQUE," + 39 | "hit_count BIGINT NOT NULL DEFAULT 0," + 40 | "require_count BIGINT NOT NULL DEFAULT 0," + 41 | "version BIGINT NOT NULL DEFAULT 0)"); 42 | 43 | return template; 44 | }; 45 | } 46 | 47 | @Override 48 | protected Stream transferResults(List> mapResults) { 49 | return mapResults.stream().map(result -> { 50 | DataDO dataDO = new DataDO(); 51 | dataDO.setHitCount((Integer) result.get("hit_count")); 52 | dataDO.setPattern((String) result.get("pattern")); 53 | dataDO.setRequireCount((Integer) result.get("require_count")); 54 | dataDO.setVersion((Integer) result.get("version")); 55 | 56 | return dataDO; 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/core/CacheXModule.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.core; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.ShootingMXBean; 5 | import com.github.cachex.reader.AbstractCacheReader; 6 | import com.github.cachex.reader.MultiCacheReader; 7 | import com.github.cachex.reader.SingleCacheReader; 8 | import com.github.jbox.utils.Collections3; 9 | import com.google.common.base.Preconditions; 10 | import com.google.inject.AbstractModule; 11 | import com.google.inject.Guice; 12 | import com.google.inject.Injector; 13 | import com.google.inject.multibindings.MapBinder; 14 | import com.google.inject.name.Names; 15 | 16 | import java.util.Optional; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | /** 20 | * @author jifang.zjf@alibaba-inc.com (FeiQing) 21 | * @version 1.0 22 | * @since 2018-05-28 17:04:00. 23 | */ 24 | public class CacheXModule extends AbstractModule { 25 | 26 | private static final AtomicBoolean init = new AtomicBoolean(false); 27 | 28 | private static Injector injector; 29 | 30 | private CacheXConfig config; 31 | 32 | private CacheXModule(CacheXConfig config) { 33 | this.config = config; 34 | } 35 | 36 | /** 37 | * 所有bean的装配工作都放到这儿 38 | */ 39 | @Override 40 | protected void configure() { 41 | Preconditions.checkArgument(config != null, "config param can not be null."); 42 | Preconditions.checkArgument(Collections3.isNotEmpty(config.getCaches()), "caches param can not be empty."); 43 | 44 | bind(CacheXConfig.class).toInstance(config); 45 | 46 | // bind caches 47 | MapBinder mapBinder = MapBinder.newMapBinder(binder(), String.class, ICache.class); 48 | config.getCaches().forEach((name, cache) -> mapBinder.addBinding(name).toInstance(cache)); 49 | 50 | // bind shootingMXBean 51 | Optional.ofNullable(config.getShootingMXBean()) 52 | .ifPresent(mxBean -> bind(ShootingMXBean.class).toInstance(mxBean)); 53 | 54 | bind(AbstractCacheReader.class).annotatedWith(Names.named("singleCacheReader")).to(SingleCacheReader.class); 55 | bind(AbstractCacheReader.class).annotatedWith(Names.named("multiCacheReader")).to(MultiCacheReader.class); 56 | } 57 | 58 | public synchronized static CacheXCore coreInstance(CacheXConfig config) { 59 | if (init.compareAndSet(false, true)) { 60 | injector = Guice.createInjector(new CacheXModule(config)); 61 | } 62 | 63 | return injector.getInstance(CacheXCore.class); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/H2ShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import org.springframework.jdbc.core.JdbcOperations; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 6 | 7 | import javax.annotation.PreDestroy; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.function.Supplier; 12 | import java.util.stream.Stream; 13 | 14 | /** 15 | * @author jifang.zjf 16 | * @since 2017/6/8 下午9:41. 17 | */ 18 | public class H2ShootingMXBeanImpl extends AbstractDBShootingMXBean { 19 | 20 | public H2ShootingMXBeanImpl() { 21 | this(System.getProperty("user.home") + "/.H2/cachex"); 22 | } 23 | 24 | public H2ShootingMXBeanImpl(String dbPath) { 25 | super(dbPath, Collections.emptyMap()); 26 | } 27 | 28 | @Override 29 | protected Supplier jdbcOperationsSupplier(String dbPath, Map context) { 30 | return () -> { 31 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 32 | dataSource.setDriverClassName("org.h2.Driver"); 33 | dataSource.setUrl(String.format("jdbc:h2:%s;AUTO_SERVER=TRUE;AUTO_RECONNECT=TRUE;AUTO_SERVER=TRUE", dbPath)); 34 | dataSource.setUsername("cachex"); 35 | dataSource.setPassword("cachex"); 36 | 37 | JdbcTemplate template = new JdbcTemplate(dataSource); 38 | template.execute("CREATE TABLE IF NOT EXISTS t_hit_rate(" + 39 | "id BIGINT IDENTITY PRIMARY KEY," + 40 | "pattern VARCHAR(64) NOT NULL UNIQUE," + 41 | "hit_count BIGINT NOT NULL DEFAULT 0," + 42 | "require_count BIGINT NOT NULL DEFAULT 0," + 43 | "version BIGINT NOT NULL DEFAULT 0)"); 44 | 45 | return template; 46 | }; 47 | } 48 | 49 | @Override 50 | protected Stream transferResults(List> mapResults) { 51 | return mapResults.stream().map((map) -> { 52 | AbstractDBShootingMXBean.DataDO dataDO = new AbstractDBShootingMXBean.DataDO(); 53 | dataDO.setPattern((String) map.get("PATTERN")); 54 | dataDO.setHitCount((long) map.get("HIT_COUNT")); 55 | dataDO.setRequireCount((long) map.get("REQUIRE_COUNT")); 56 | dataDO.setVersion((long) map.get("VERSION")); 57 | 58 | return dataDO; 59 | }); 60 | } 61 | 62 | @PreDestroy 63 | public void tearDown() { 64 | super.tearDown(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/resources/spring-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/cases/MultiTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.cases; 2 | 3 | import com.github.cachex.Utils; 4 | import com.github.cachex.cases.base.TestBase; 5 | import com.github.cachex.domain.User; 6 | import com.github.cachex.service.UserService; 7 | import com.google.common.collect.Lists; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import java.util.List; 12 | import java.util.Random; 13 | import java.util.stream.Stream; 14 | 15 | import static java.util.stream.Collectors.toList; 16 | 17 | /** 18 | * @author jifang 19 | * @since 2016/11/30 下午2:19. 20 | */ 21 | public class MultiTest extends TestBase { 22 | 23 | @Autowired 24 | private UserService service; 25 | 26 | @Test 27 | public void testReturnList() { 28 | System.out.println("#########---"); 29 | List ids = Lists.newArrayList(1, 2, 3, 4); 30 | service.getUsers(ids, "ss", new Object()); 31 | System.out.println("---"); 32 | service.getUsers(ids, "ss", new Object()); 33 | System.out.println("---"); 34 | ids.add(new Random().nextInt()); 35 | List users = service.getUsers(ids, "ss", new Object()); 36 | System.out.println("---"); 37 | service.invalidList(users); 38 | System.out.println("---"); 39 | service.getUsers(ids, "ss", new Object()); 40 | System.out.println("#########---"); 41 | } 42 | 43 | @Test 44 | public void testReturnMap() { 45 | System.out.println("#########---"); 46 | List ids = Lists.newArrayList(0, 1, 2); 47 | service.returnMap("name", ids, "ok"); 48 | System.out.println("---"); 49 | service.returnMap("name", ids, "ok"); 50 | System.out.println("---"); 51 | ids.add(new Random().nextInt()); 52 | service.returnMap("name", ids, "ok"); 53 | System.out.println("---"); 54 | service.invalidMap("name", ids); 55 | System.out.println("---"); 56 | service.returnMap("name", ids, "ok"); 57 | System.out.println("---"); 58 | service.returnMap("name", ids, "ok"); 59 | System.out.println("#########---"); 60 | } 61 | 62 | @Test 63 | public void testRandomGet() { 64 | 65 | 66 | for (int i = 0; i < 50; ++i) { 67 | // List ids = Stream.generate(this::nextRandom).limit(nextLitterRandom()).collect(Collectors.toList()); 68 | List collect = Stream.generate(this::nextRandom).limit(100).collect(toList()); 69 | service.getUsers(collect, "name", new Object()); 70 | // service.returnMap("app", ids, new Object()); 71 | 72 | Utils.delay(100); 73 | } 74 | } 75 | 76 | private int nextRandom() { 77 | return (int) (Math.random() * 100); 78 | } 79 | 80 | private int nextLitterRandom() { 81 | return (int) (Math.random() * 100); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/RedisClusterCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.enums.Expire; 5 | import com.github.jbox.serializer.ISerializer; 6 | import com.github.jbox.serializer.support.Hessian2Serializer; 7 | import redis.clients.jedis.JedisCluster; 8 | 9 | import javax.annotation.PreDestroy; 10 | import java.io.IOException; 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static com.github.cachex.support.cache.RedisHelpers.toByteArray; 17 | import static com.github.cachex.support.cache.RedisHelpers.toObjectMap; 18 | 19 | /** 20 | * @author jifang 21 | * @since 2017/4/6 下午4:24. 22 | */ 23 | public class RedisClusterCache implements ICache { 24 | 25 | private ISerializer serializer; 26 | 27 | private JedisCluster jedisCluster; 28 | 29 | public RedisClusterCache(JedisCluster jedisCluster) { 30 | this(jedisCluster, new Hessian2Serializer()); 31 | } 32 | 33 | public RedisClusterCache(JedisCluster jedisCluster, ISerializer serializer) { 34 | this.jedisCluster = jedisCluster; 35 | this.serializer = serializer; 36 | } 37 | 38 | @Override 39 | public Object read(String key) { 40 | return serializer.deserialize(jedisCluster.get(key.getBytes())); 41 | } 42 | 43 | @Override 44 | public void write(String key, Object value, long expire) { 45 | byte[] bytes = serializer.serialize(value); 46 | if (expire == Expire.FOREVER) { 47 | jedisCluster.set(key.getBytes(), bytes); 48 | } else { 49 | jedisCluster.setex(key.getBytes(), (int) (expire / 1000), bytes); 50 | } 51 | } 52 | 53 | @Override 54 | public Map read(Collection keys) { 55 | if (keys.isEmpty()) { 56 | return Collections.emptyMap(); 57 | } 58 | 59 | List bytesValues = jedisCluster.mget(toByteArray(keys)); 60 | return toObjectMap(keys, bytesValues, this.serializer); 61 | } 62 | 63 | @Override 64 | public void write(Map keyValueMap, long expire) { 65 | if (keyValueMap.isEmpty()) { 66 | return; 67 | } 68 | 69 | if (expire == Expire.FOREVER) { 70 | jedisCluster.mset(toByteArray(keyValueMap, this.serializer)); 71 | } else { 72 | for (Map.Entry entry : keyValueMap.entrySet()) { 73 | write(entry.getKey(), entry.getValue(), expire); 74 | } 75 | } 76 | } 77 | 78 | @Override 79 | public void remove(String... keys) { 80 | if (keys.length == 0) { 81 | return; 82 | } 83 | 84 | jedisCluster.del(keys); 85 | } 86 | 87 | 88 | @PreDestroy 89 | public void tearDown() throws IOException { 90 | if (this.jedisCluster != null) { 91 | this.jedisCluster.close(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/reader/SingleCacheReader.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.reader; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | import com.github.cachex.core.CacheXConfig; 5 | import com.github.cachex.domain.CacheXAnnoHolder; 6 | import com.github.cachex.domain.CacheXMethodHolder; 7 | import com.github.cachex.invoker.Invoker; 8 | import com.github.cachex.manager.CacheManager; 9 | import com.github.cachex.utils.PatternGenerator; 10 | import com.github.cachex.utils.PreventObjects; 11 | import com.github.cachex.utils.CacheXLogger; 12 | import com.github.cachex.utils.KeyGenerator; 13 | import com.google.inject.Inject; 14 | import com.google.inject.Singleton; 15 | 16 | /** 17 | * @author jifang 18 | * @since 2016/11/5 下午3:10. 19 | */ 20 | @Singleton 21 | public class SingleCacheReader extends AbstractCacheReader { 22 | 23 | @Inject 24 | private CacheManager cacheManager; 25 | 26 | @Inject 27 | private CacheXConfig config; 28 | 29 | @Inject(optional = true) 30 | private ShootingMXBean shootingMXBean; 31 | 32 | @Override 33 | public Object read(CacheXAnnoHolder cacheXAnnoHolder, CacheXMethodHolder cacheXMethodHolder, Invoker invoker, boolean needWrite) throws Throwable { 34 | String key = KeyGenerator.generateSingleKey(cacheXAnnoHolder, invoker.getArgs()); 35 | Object readResult = cacheManager.readSingle(cacheXAnnoHolder.getCache(), key); 36 | 37 | doRecord(readResult, key, cacheXAnnoHolder); 38 | // 命中 39 | if (readResult != null) { 40 | // 是放击穿对象 41 | if (PreventObjects.isPrevent(readResult)) { 42 | return null; 43 | } 44 | 45 | return readResult; 46 | } 47 | 48 | 49 | // not hit 50 | // invoke method 51 | Object invokeResult = doLogInvoke(invoker::proceed); 52 | if (invokeResult != null && cacheXMethodHolder.getInnerReturnType() == null) { 53 | cacheXMethodHolder.setInnerReturnType(invokeResult.getClass()); 54 | } 55 | 56 | if (!needWrite) { 57 | return invokeResult; 58 | } 59 | 60 | if (invokeResult != null) { 61 | cacheManager.writeSingle(cacheXAnnoHolder.getCache(), key, invokeResult, cacheXAnnoHolder.getExpire()); 62 | return invokeResult; 63 | } 64 | 65 | // invokeResult is null 66 | if (config.isPreventOn()) { 67 | cacheManager.writeSingle(cacheXAnnoHolder.getCache(), key, PreventObjects.getPreventObject(), cacheXAnnoHolder.getExpire()); 68 | } 69 | 70 | return null; 71 | } 72 | 73 | private void doRecord(Object result, String key, CacheXAnnoHolder cacheXAnnoHolder) { 74 | CacheXLogger.info("single cache hit rate: {}/1, key: {}", result == null ? 0 : 1, key); 75 | if (this.shootingMXBean != null) { 76 | String pattern = PatternGenerator.generatePattern(cacheXAnnoHolder); 77 | 78 | if (result != null) { 79 | this.shootingMXBean.hitIncr(pattern, 1); 80 | } 81 | this.shootingMXBean.reqIncr(pattern, 1); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/ResultUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author jifang 8 | * @since 2016/11/18 下午3:43. 9 | */ 10 | public class ResultUtils { 11 | 12 | public static Map mergeMap(Class resultMapType, 13 | Map proceedEntryValueMap, 14 | Map key2MultiEntry, 15 | Map hitKeyValueMap) { 16 | 17 | Map resultMap = Addables.newMap(resultMapType, proceedEntryValueMap); 18 | mergeCacheValueToResultMap(resultMap, hitKeyValueMap, key2MultiEntry); 19 | return resultMap; 20 | } 21 | 22 | public static Map toMap(Class resultMapType, 23 | Map key2MultiEntry, 24 | Map hitKeyValueMap) { 25 | Map resultMap = Addables.newMap(resultMapType, null); 26 | mergeCacheValueToResultMap(resultMap, hitKeyValueMap, key2MultiEntry); 27 | return resultMap; 28 | } 29 | 30 | // 将缓存命中的内容都合并到返回值内 31 | private static void mergeCacheValueToResultMap(Map resultMap, 32 | Map hitKeyValueMap, 33 | Map key2MultiEntry) { 34 | for (Map.Entry entry : hitKeyValueMap.entrySet()) { 35 | Object inCacheValue = entry.getValue(); 36 | if (PreventObjects.isPrevent(inCacheValue)) { 37 | continue; 38 | } 39 | 40 | String cacheKey = entry.getKey(); 41 | Object multiArgEntry = key2MultiEntry.get(cacheKey); 42 | 43 | resultMap.put(multiArgEntry, inCacheValue); 44 | } 45 | } 46 | 47 | 48 | public static Collection mergeCollection(Class collectionType, 49 | Collection proceedCollection, 50 | Map hitKeyValueMap) { 51 | Collection resultCollection = Addables.newCollection(collectionType, proceedCollection); 52 | mergeCacheValueToResultCollection(resultCollection, hitKeyValueMap); 53 | return resultCollection; 54 | } 55 | 56 | public static Collection toCollection(Class collectionType, 57 | Map hitKeyValueMap) { 58 | 59 | Collection resultCollection = Addables.newCollection(collectionType, null); 60 | 61 | mergeCacheValueToResultCollection(resultCollection, hitKeyValueMap); 62 | 63 | return resultCollection; 64 | } 65 | 66 | private static void mergeCacheValueToResultCollection(Collection resultCollection, 67 | Map hitKeyValueMap) { 68 | for (Object inCacheValue : hitKeyValueMap.values()) { 69 | if (PreventObjects.isPrevent(inCacheValue)) { 70 | continue; 71 | } 72 | 73 | resultCollection.add(inCacheValue); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/RedisPoolCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.enums.Expire; 5 | import com.github.jbox.serializer.ISerializer; 6 | import com.github.jbox.serializer.support.Hessian2Serializer; 7 | import redis.clients.jedis.Jedis; 8 | import redis.clients.jedis.JedisPool; 9 | import redis.clients.jedis.Pipeline; 10 | 11 | import javax.annotation.PreDestroy; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static com.github.cachex.support.cache.RedisHelpers.toByteArray; 17 | import static com.github.cachex.support.cache.RedisHelpers.toObjectMap; 18 | 19 | /** 20 | * @author jifang 21 | * @since 2016/12/12 下午3:06. 22 | */ 23 | public class RedisPoolCache implements ICache { 24 | 25 | private ISerializer serializer; 26 | 27 | private JedisPool jedisPool; 28 | 29 | public RedisPoolCache(JedisPool jedisPool) { 30 | this(jedisPool, new Hessian2Serializer()); 31 | } 32 | 33 | public RedisPoolCache(JedisPool jedisPool, ISerializer serializer) { 34 | this.jedisPool = jedisPool; 35 | this.serializer = serializer; 36 | } 37 | 38 | @Override 39 | public Object read(String key) { 40 | try (Jedis client = jedisPool.getResource()) { 41 | byte[] bytes = client.get(key.getBytes()); 42 | return serializer.deserialize(bytes); 43 | } 44 | } 45 | 46 | @Override 47 | public void write(String key, Object value, long expire) { 48 | try (Jedis client = jedisPool.getResource()) { 49 | byte[] bytesValue = serializer.serialize(value); 50 | if (expire == Expire.FOREVER) { 51 | client.set(key.getBytes(), bytesValue); 52 | } else { 53 | client.psetex(key.getBytes(), expire, bytesValue); 54 | } 55 | } 56 | } 57 | 58 | @Override 59 | public Map read(Collection keys) { 60 | try (Jedis client = jedisPool.getResource()) { 61 | List bytesValues = client.mget(toByteArray(keys)); 62 | return toObjectMap(keys, bytesValues, this.serializer); 63 | } 64 | } 65 | 66 | @Override 67 | public void write(Map keyValueMap, long expire) { 68 | try (Jedis client = jedisPool.getResource()) { 69 | byte[][] kvs = toByteArray(keyValueMap, serializer); 70 | if (expire == Expire.FOREVER) { 71 | client.mset(kvs); 72 | } else { 73 | Pipeline pipeline = client.pipelined(); 74 | for (int i = 0; i < kvs.length; i += 2) { 75 | pipeline.psetex(kvs[i], expire, kvs[i + 1]); 76 | } 77 | pipeline.sync(); 78 | } 79 | } 80 | } 81 | 82 | @Override 83 | public void remove(String... keys) { 84 | try (Jedis client = jedisPool.getResource()) { 85 | client.del(keys); 86 | } 87 | } 88 | 89 | @PreDestroy 90 | public void tearDown() { 91 | if (jedisPool != null && !jedisPool.isClosed()) { 92 | jedisPool.destroy(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/CacheXProxy.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex; 2 | 3 | import com.github.cachex.core.CacheXConfig; 4 | import com.github.cachex.core.CacheXCore; 5 | import com.github.cachex.core.CacheXModule; 6 | import com.github.cachex.invoker.adapter.InvocationInvokerAdapter; 7 | import org.apache.commons.proxy.Interceptor; 8 | import org.apache.commons.proxy.Invocation; 9 | import org.apache.commons.proxy.ProxyFactory; 10 | import org.apache.commons.proxy.factory.cglib.CglibProxyFactory; 11 | import org.springframework.beans.factory.FactoryBean; 12 | 13 | import java.lang.reflect.Method; 14 | import java.util.Map; 15 | 16 | /** 17 | * @author jifang.zjf 18 | * @since 2017/6/22 上午11:04. 19 | */ 20 | @SuppressWarnings("unchecked") 21 | public class CacheXProxy implements FactoryBean { 22 | 23 | private Object target; 24 | 25 | private Object proxy; 26 | 27 | private Class type; 28 | 29 | private CacheXConfig.Switch cglib = CacheXConfig.Switch.OFF; 30 | 31 | private CacheXCore cacheXCore; 32 | 33 | public CacheXProxy(Object target, Map caches) { 34 | this(target, (Class) target.getClass().getInterfaces()[0], caches, CacheXConfig.Switch.OFF); 35 | } 36 | 37 | public CacheXProxy(Object target, Class type, Map caches, CacheXConfig.Switch cglib) { 38 | this.target = target; 39 | this.type = type; 40 | this.cglib = cglib; 41 | this.proxy = newProxy(); 42 | this.cacheXCore = CacheXModule.coreInstance(CacheXConfig.newConfig(caches)); 43 | } 44 | 45 | private Object newProxy() { 46 | ProxyFactory factory; 47 | if (cglib == CacheXConfig.Switch.ON || !this.type.isInterface()) { 48 | factory = new CglibProxyFactory(); 49 | } else { 50 | factory = new ProxyFactory(); 51 | } 52 | 53 | return factory.createInterceptorProxy(target, interceptor, new Class[]{type}); 54 | } 55 | 56 | private Interceptor interceptor = new Interceptor() { 57 | 58 | @Override 59 | public Object intercept(Invocation invocation) throws Throwable { 60 | 61 | Method method = invocation.getMethod(); 62 | Cached cached; 63 | if ((cached = method.getAnnotation(Cached.class)) != null) { 64 | return cacheXCore.readWrite(cached, method, new InvocationInvokerAdapter(target, invocation)); 65 | } 66 | 67 | CachedGet cachedGet; 68 | if ((cachedGet = method.getAnnotation(CachedGet.class)) != null) { 69 | return cacheXCore.read(cachedGet, method, new InvocationInvokerAdapter(target, invocation)); 70 | } 71 | 72 | Invalid invalid; 73 | if ((invalid = method.getAnnotation(Invalid.class)) != null) { 74 | cacheXCore.remove(invalid, method, invocation.getArguments()); 75 | return null; 76 | } 77 | 78 | return invocation.proceed(); 79 | } 80 | }; 81 | 82 | @Override 83 | public T getObject() { 84 | return (T) proxy; 85 | } 86 | 87 | @Override 88 | public Class getObjectType() { 89 | return type; 90 | } 91 | 92 | @Override 93 | public boolean isSingleton() { 94 | return true; 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/LevelDBCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.jbox.serializer.ISerializer; 5 | import com.github.jbox.serializer.support.Hessian2Serializer; 6 | import org.iq80.leveldb.CompressionType; 7 | import org.iq80.leveldb.DB; 8 | import org.iq80.leveldb.Options; 9 | import org.iq80.leveldb.WriteBatch; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.annotation.PreDestroy; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.Collection; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.Optional; 20 | 21 | import static org.iq80.leveldb.impl.Iq80DBFactory.factory; 22 | 23 | /** 24 | * @author jifang.zjf 25 | * @since 2017/6/9 上午10:52. 26 | */ 27 | public class LevelDBCache implements ICache { 28 | 29 | private static final Logger LOGGER = LoggerFactory.getLogger(LevelDBCache.class); 30 | 31 | private DB levelDB; 32 | 33 | private ISerializer serializer; 34 | 35 | public LevelDBCache() throws IOException { 36 | this(new Hessian2Serializer()); 37 | } 38 | 39 | public LevelDBCache(String levelFilePath) throws IOException { 40 | this(levelFilePath, new Hessian2Serializer()); 41 | } 42 | 43 | public LevelDBCache(ISerializer serializer) throws IOException { 44 | this(System.getProperty("user.home") + "/.LevelDB/", serializer); 45 | } 46 | 47 | public LevelDBCache(String levelFilePath, ISerializer serializer) throws IOException { 48 | Options options = new Options(); 49 | options.compressionType(CompressionType.SNAPPY); 50 | options.createIfMissing(true); 51 | this.levelDB = factory.open(new File(levelFilePath), options); 52 | this.serializer = Optional.ofNullable(serializer).orElseThrow(NullPointerException::new); 53 | } 54 | 55 | @Override 56 | public Object read(String key) { 57 | return serializer.deserialize(levelDB.get(key.getBytes())); 58 | } 59 | 60 | @Override 61 | public void write(String key, Object value, long expire) { 62 | levelDB.put(key.getBytes(), serializer.serialize(value)); 63 | } 64 | 65 | @Override 66 | public Map read(Collection keys) { 67 | Map result = new HashMap<>(keys.size()); 68 | keys.forEach((key) -> result.put(key, read(key))); 69 | 70 | return result; 71 | } 72 | 73 | @Override 74 | public void write(Map keyValueMap, long expire) { 75 | WriteBatch updates = levelDB.createWriteBatch(); 76 | 77 | keyValueMap.forEach((key, value) -> updates.put(key.getBytes(), serializer.serialize(value))); 78 | 79 | levelDB.write(updates); 80 | } 81 | 82 | @Override 83 | public void remove(String... keys) { 84 | for (String key : keys) { 85 | levelDB.delete(key.getBytes()); 86 | } 87 | } 88 | 89 | @PreDestroy 90 | public void tearDown() { 91 | if (levelDB != null) { 92 | try { 93 | levelDB.close(); 94 | } catch (IOException e) { 95 | LOGGER.error("LevelDB close error", e); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/EhCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.google.common.collect.Sets; 5 | import org.ehcache.Cache; 6 | import org.ehcache.PersistentCacheManager; 7 | import org.ehcache.config.CacheConfiguration; 8 | import org.ehcache.config.ResourcePools; 9 | import org.ehcache.config.builders.CacheConfigurationBuilder; 10 | import org.ehcache.config.builders.CacheManagerBuilder; 11 | import org.ehcache.config.builders.ResourcePoolsBuilder; 12 | import org.ehcache.config.units.EntryUnit; 13 | import org.ehcache.config.units.MemoryUnit; 14 | 15 | import javax.annotation.PreDestroy; 16 | import java.io.Serializable; 17 | import java.util.Collection; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author jifang 23 | * @since 2017/1/10 上午11:18. 24 | */ 25 | public class EhCache implements ICache { 26 | 27 | private PersistentCacheManager cacheManager; 28 | private Cache ehcache; 29 | 30 | public EhCache(long heapEntries, long offHeapMBSize, long diskGBSize) { 31 | this(heapEntries, offHeapMBSize, System.getProperty("user.home") + "/.EhCache", diskGBSize); 32 | } 33 | 34 | public EhCache(long heapEntries, long offHeapMBSize, String diskPath, long diskGBSize) { 35 | 36 | ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder() 37 | .heap(heapEntries, EntryUnit.ENTRIES) 38 | .offheap(offHeapMBSize, MemoryUnit.MB) 39 | .disk(diskGBSize, MemoryUnit.GB) 40 | .build(); 41 | 42 | CacheConfiguration configuration = CacheConfigurationBuilder 43 | .newCacheConfigurationBuilder(String.class, Serializable.class, resourcePools) 44 | .build(); 45 | 46 | cacheManager = CacheManagerBuilder.newCacheManagerBuilder() 47 | .with(CacheManagerBuilder.persistence(diskPath)) 48 | .withCache("cachex", configuration) 49 | .build(true); 50 | 51 | ehcache = cacheManager.getCache("cachex", String.class, Serializable.class); 52 | } 53 | 54 | @Override 55 | public Object read(String key) { 56 | return ehcache.get(key); 57 | } 58 | 59 | @Override 60 | public void write(String key, Object value, long expire) { 61 | ehcache.put(key, (Serializable) value); 62 | } 63 | 64 | @Override 65 | public Map read(Collection keys) { 66 | Map map = new HashMap<>(keys.size()); 67 | for (String key : keys) { 68 | map.put(key, ehcache.get(key)); 69 | } 70 | 71 | return map; 72 | } 73 | 74 | @Override 75 | public void write(Map keyValueMap, long expire) { 76 | Map map = new HashMap<>(keyValueMap.size()); 77 | for (Map.Entry entry : keyValueMap.entrySet()) { 78 | Object value = entry.getValue(); 79 | map.put(entry.getKey(), (Serializable) value); 80 | } 81 | ehcache.putAll(map); 82 | } 83 | 84 | @Override 85 | public void remove(String... keys) { 86 | ehcache.removeAll(Sets.newHashSet(keys)); 87 | } 88 | 89 | @PreDestroy 90 | public void tearDown() { 91 | if (this.cacheManager != null) { 92 | this.cacheManager.close(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/domain/CacheXAnnoHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.domain; 2 | 3 | import com.github.cachex.CacheKey; 4 | 5 | import java.lang.reflect.Method; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author jifang 10 | * @since 2016/11/29 下午10:43. 11 | */ 12 | public class CacheXAnnoHolder { 13 | 14 | private Method method; 15 | 16 | // ******************* // 17 | // --- cached 内容 ---- // 18 | // ******************* // 19 | private String cache; 20 | 21 | private String prefix; 22 | 23 | private int expire; 24 | 25 | 26 | // ****************** // 27 | // --- @CacheKey --- // 28 | // ****************** // 29 | private Map cacheKeyMap; 30 | 31 | private int multiIndex = -1; 32 | 33 | private String id; 34 | 35 | private CacheXAnnoHolder(Method method, 36 | String cache, String prefix, int expire, 37 | Map cacheKeyMap, int multiIndex, String id) { 38 | this.method = method; 39 | this.cache = cache; 40 | this.prefix = prefix; 41 | this.expire = expire; 42 | this.cacheKeyMap = cacheKeyMap; 43 | this.multiIndex = multiIndex; 44 | this.id = id; 45 | } 46 | 47 | public Method getMethod() { 48 | return method; 49 | } 50 | 51 | public String getCache() { 52 | return cache; 53 | } 54 | 55 | public String getPrefix() { 56 | return prefix; 57 | } 58 | 59 | public int getExpire() { 60 | return expire; 61 | } 62 | 63 | public Map getCacheKeyMap() { 64 | return cacheKeyMap; 65 | } 66 | 67 | public int getMultiIndex() { 68 | return multiIndex; 69 | } 70 | 71 | public boolean isMulti() { 72 | return multiIndex != -1; 73 | } 74 | 75 | public String getId() { 76 | return id; 77 | } 78 | 79 | public static class Builder { 80 | 81 | private Method method; 82 | 83 | private String cache; 84 | 85 | private String prefix; 86 | 87 | private int expire; 88 | 89 | private Map cacheKeyMap; 90 | 91 | private int multiIndex = -1; 92 | 93 | private String id; 94 | 95 | private Builder(Method method) { 96 | this.method = method; 97 | } 98 | 99 | public static Builder newBuilder(Method method) { 100 | return new Builder(method); 101 | } 102 | 103 | public Builder setCache(String cache) { 104 | this.cache = cache; 105 | return this; 106 | } 107 | 108 | public Builder setPrefix(String prefix) { 109 | this.prefix = prefix; 110 | return this; 111 | } 112 | 113 | public Builder setExpire(int expire) { 114 | this.expire = expire; 115 | return this; 116 | } 117 | 118 | public Builder setMultiIndex(int multiIndex) { 119 | this.multiIndex = multiIndex; 120 | return this; 121 | } 122 | 123 | public Builder setId(String id) { 124 | this.id = id; 125 | return this; 126 | } 127 | 128 | public Builder setCacheKeyMap(Map cacheKeyMap) { 129 | this.cacheKeyMap = cacheKeyMap; 130 | return this; 131 | } 132 | 133 | public CacheXAnnoHolder build() { 134 | return new CacheXAnnoHolder(method, cache, prefix, expire, cacheKeyMap, multiIndex, id); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /markdown/release.md: -------------------------------------------------------------------------------- 1 | ## 重大版本历史 2 | 3 | ### 0.0.x 4 | - 最初的redis-annotation, 作为一个RedisClusterClient的封装提供给如下应用使用 5 | - feedcenter 6 | - feedcenter-push 7 | - feedcenter-admin 8 | - 文档 9 | - [redis-annotation 文档](https://github.com/feiqing/CacheX/wiki/redisCli-annotation-%E6%96%87%E6%A1%A3) 10 | - [redis-annotation 分享](https://github.com/feiqing/CacheX/wiki/redisCli-annotation-%E5%88%86%E4%BA%AB) 11 | - 性能要求 12 | - 动态中心系统三个应用350W+次Dubbo调用, 2.6亿+次缓存读写, 单次查询(get/mget)耗时 0~2ms (1~200个key); 13 | 14 | --- 15 | ### 1.0.x(0.1.x) 16 | - 1.0.0 17 | - 重构为CacheX: 开放`ICache`接口, 不再强依赖某一特定缓存产品, 而是作为`ICache`接口的一个具体实现, 以支持更多的缓存服务(如Memcached、Redis、Guava、`ConcurrentHashMap`...); 18 | - 1.0.1 19 | - 添加全局缓存开关`cachex` Switch, 支持CacheX动态开关; 20 | - 1.0.2 21 | - 开放`com.github.cachex.ISerializer`缓存序列化接口; 22 | 23 | --- 24 | ### 1.2.x(0.3.x) 25 | - `@Cached`/`@Invalidate`添加`value`属性, 使CacheX支持管理多个缓存实现; 26 | - `@Cached`添加`condition`属性: 条件缓存, 支持SpEL表达式, 当表达式结果为`true`时缓存; 27 | - `@Cached`添加`prefix`属性, 为为当前方法的所有缓存添加统一前缀, 且支持无参方法缓存; 28 | 29 | --- 30 | ### 1.3.x 31 | - 支持缓存命中率分组统计功能, 添加JMX支持, 可以详细查看各类业务缓存命中情况, 便于缓存优化, 提升系统吞吐: 32 | - 支持查看/重置**全部**key命中率; 33 | - 支持查看/重置**分组**key命中率; 34 | 35 | --- 36 | ### 1.4.x 37 | - CacheX部分配置代码重构, 自定义IoC容器: 使`@Singleton`, `@Inject`生效; 38 | 39 | --- 40 | ### 1.5.x 41 | - 1.5.1 42 | - 抽象并开放出`com.github.cachex.ShootingMXBean`接口, 支持自定义缓存分组命中率统计实现; 43 | - 添加`MemoryShootingMXBeanImpl`, 支持基于内存计数的缓存命中率统计((以机器为单位, 重启历史数据丢失)); 44 | - 1.5.2 45 | - 添加`DerbyShootingMXBeanImpl`、`H2ShootingMXBeanImpl`实现, 支持基于嵌入式DB的缓存命中率统计(以机器为单位, 重启历史数据不丢失; 其中Derby实现可以动态加载jdk提供的derby.jar包, 实现0依赖配置) 46 | - 添加`ZKShootingMXBeanImpl`实现, 支持基于ZooKeeper的异步命中率统计, 可以做到统一应用共享计数器(以应用为单位, 重启历史数据不丢失); 47 | - 1.5.4 48 | - 消除[限制4](limit.md#4-各类怪异的内部容器类调用), 支持: 49 | 50 | | Map | Collection | 51 | :------- | ------- | 52 | | `Collections.emptyMap()` | `Collections.emptyList()` | 53 | | `Collections.emptyNavigableMap()` | `Collections.emptySet()` | 54 | | `Collections.emptySortedMap()` | `Collections.emptySortedSet()` | 55 | | `Collections.singletonMap()` | `Collections.emptyNavigableSet()` | 56 | | `Collections.unmodifiableMap()` | `Collections.singletonList()` | 57 | | `Collections.unmodifiableNavigableMap()` | `Collections.singleton()` | 58 | | `Collections.unmodifiableSortedMap()` | `Arrays.asList()` | 59 | | `Collections.synchronizedMap()` | `Collections.unmodifiableCollection()` | 60 | | `Collections.synchronizedNavigableMap()` | `Collections.unmodifiableList()` | 61 | | `Collections.synchronizedSortedMap()` | `Collections.unmodifiableSet()` | 62 | | `Collections.checkedMap()` | `Collections.unmodifiableSortedSet()` | 63 | | `Collections.checkedNavigableMap()` | `Collections.unmodifiableNavigableSet()` | 64 | | `Collections.checkedSortedMap()` | `Collections.synchronizedCollection()` | 65 | | | `Collections.synchronizedList()` | 66 | | | `Collections.synchronizedSet()` | 67 | | | `Collections.synchronizedNavigableSet()` | 68 | | | `Collections.synchronizedSortedSet()` | 69 | | | `Collections.checkedCollection()` | 70 | | | `Collections.checkedList()` | 71 | | | `Collections.checkedSet()` | 72 | | | `Collections.checkedNavigableSet()` | 73 | | | `Collections.checkedSortedSet()` | 74 | | | `Collections.checkedQueue()` | 75 | - 1.5.5 76 | - 删除`@Cached`/`@Invalid`/`@CachedGet`内的`seperator`属性, 似缓存key拼装更简单 77 | 78 | ### 1.6.x 79 | - 添加缓存防击穿策略(开启后: 如果执行方法返回`null`, 则向缓存内写入一个空对象, 下次不走DB) 80 | - `ISerializer`抽取为`com.github.jbox.serializer.ISerializer`, 放入jbox内, CacheX开始依赖jbox-1.6 81 | 82 | ### 1.7.x 83 | - 将`CacheXAspect`拆分成为`CacheXAspect`与`CacheXCore`, 并添加`CacheXProxy`, 84 | 使CacheX的核心保持在`CacheXCore`至`ICache`以上: 85 | - 在`CacheXCore`以上可以任意添加框架的门面; 86 | - 在`ICache`以下可以任意添加缓存服务的实现; 87 | - 将自研IoC替换为Google Guice. 88 | - 专注于稳定、高性能的缓存框架. 89 | 90 | --- -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/MemcachedCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.enums.Expire; 5 | import com.github.cachex.exception.CacheXException; 6 | import com.github.jbox.serializer.ISerializer; 7 | import com.github.jbox.serializer.support.Hessian2Serializer; 8 | import net.rubyeye.xmemcached.MemcachedClient; 9 | import net.rubyeye.xmemcached.XMemcachedClientBuilder; 10 | import net.rubyeye.xmemcached.exception.MemcachedException; 11 | 12 | import javax.annotation.PreDestroy; 13 | import java.io.IOException; 14 | import java.util.Collection; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.concurrent.TimeoutException; 18 | 19 | /** 20 | * @author jifang 21 | * @since 2016/12/12 下午4:05. 22 | */ 23 | public class MemcachedCache implements ICache { 24 | 25 | private static final int _30_DAYS = 30 * 24 * 60 * 60; 26 | 27 | private MemcachedClient client; 28 | 29 | private ISerializer serializer; 30 | 31 | public MemcachedCache(String ipPorts) throws IOException { 32 | this(ipPorts, new Hessian2Serializer()); 33 | } 34 | 35 | public MemcachedCache(String addressList, ISerializer serializer) throws IOException { 36 | client = new XMemcachedClientBuilder(addressList).build(); 37 | this.serializer = serializer; 38 | } 39 | 40 | @Override 41 | public Object read(String key) { 42 | try { 43 | byte[] bytes = client.get(key); 44 | return serializer.deserialize(bytes); 45 | } catch (TimeoutException | InterruptedException | MemcachedException e) { 46 | throw new CacheXException(e); 47 | } 48 | } 49 | 50 | @Override 51 | public void write(String key, Object value, long expire) { 52 | byte[] byteValue = serializer.serialize(value); 53 | try { 54 | if (expire == Expire.FOREVER) { 55 | client.set(key, _30_DAYS, byteValue); 56 | } else { 57 | client.set(key, (int) (expire/1000), byteValue); 58 | } 59 | } catch (TimeoutException | InterruptedException | MemcachedException e) { 60 | throw new CacheXException(e); 61 | } 62 | } 63 | 64 | @Override 65 | public Map read(Collection keys) { 66 | try { 67 | Map byteMap = client.get(keys); 68 | Map resultMap = new HashMap<>(byteMap.size()); 69 | for (Map.Entry entry : byteMap.entrySet()) { 70 | String key = entry.getKey(); 71 | Object value = serializer.deserialize(entry.getValue()); 72 | 73 | resultMap.put(key, value); 74 | } 75 | 76 | return resultMap; 77 | } catch (TimeoutException | InterruptedException | MemcachedException e) { 78 | throw new CacheXException(e); 79 | } 80 | } 81 | 82 | @Override 83 | public void write(Map keyValueMap, long expire) { 84 | for (Map.Entry entry : keyValueMap.entrySet()) { 85 | this.write(entry.getKey(), entry.getValue(), expire); 86 | } 87 | } 88 | 89 | @Override 90 | public void remove(String... keys) { 91 | try { 92 | for (String key : keys) { 93 | client.delete(key); 94 | } 95 | } catch (TimeoutException | InterruptedException | MemcachedException e) { 96 | throw new CacheXException(e); 97 | } 98 | } 99 | 100 | @PreDestroy 101 | public void tearDown() { 102 | if (client != null && !client.isShutdown()) { 103 | try { 104 | client.shutdown(); 105 | } catch (IOException e) { 106 | throw new CacheXException(e); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/github/cachex/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.service.impl; 2 | 3 | 4 | import com.github.cachex.CacheKey; 5 | import com.github.cachex.Cached; 6 | import com.github.cachex.Invalid; 7 | import com.github.cachex.domain.User; 8 | import com.github.cachex.enums.Expire; 9 | import com.github.cachex.service.UserService; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.*; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * @author jifang 17 | * @since 16/3/20 下午5:49. 18 | */ 19 | @Service 20 | public class UserServiceImpl implements UserService { 21 | 22 | /** 23 | * multi 24 | ***/ 25 | @Override 26 | @Cached(prefix = "map:", expire = Expire.TEN_MIN) 27 | public Map returnMap(@CacheKey String app, @CacheKey("'-' + #arg1[#i]") List ids, Object noKey) { 28 | Map map = new HashMap<>(); 29 | for (Integer id : ids) { 30 | map.put(id, new User(id, "name" + id, new Date(), id, noKey.toString())); 31 | } 32 | return map; 33 | } 34 | 35 | @Override 36 | @Invalid(prefix = "map:") 37 | public void invalidMap(@CacheKey String apps, @CacheKey("'-' + #arg1[#i]") List ids) { 38 | System.out.println("method: " + ids); 39 | } 40 | 41 | @Override 42 | @Cached(prefix = "[USER]:") 43 | public List getUsers(@CacheKey(value = "#arg0[#i]", field = "id") List ids, 44 | @CacheKey("'-' + #arg1") String name, Object non) { 45 | return ids.stream().map((id) -> new User(id, name)).collect(Collectors.toList()); 46 | } 47 | 48 | @Override 49 | @Invalid(prefix = "[USER]:") 50 | public void invalidList(@CacheKey(value = "#arg0[#i].id + '-' + #arg0[#i].name") List users) { 51 | List ids = new ArrayList<>(users.size()); 52 | for (User user : users) { 53 | ids.add(user.getId()); 54 | } 55 | System.out.println("method:" + ids); 56 | } 57 | 58 | 59 | /** 60 | * single 61 | */ 62 | 63 | @Cached(prefix = "list:", expire = Expire.TEN_SEC) 64 | public User singleKey(@CacheKey int id, String name, Object non) { 65 | return new User(id, name, new Date(), 1, "山东-德州"); 66 | } 67 | 68 | @Override 69 | @Invalid(prefix = "list:") 70 | public void singleRemove(@CacheKey int id, String name, Object non) { 71 | } 72 | 73 | @Override 74 | @Invalid(prefix = "list") 75 | public void updateUser(@CacheKey(value = "id") User user, @CacheKey String name, Object non) { 76 | } 77 | 78 | @Override 79 | @Cached 80 | public boolean spelCompose(@CacheKey(value = "'id:'+#arg0.id+'-name:'+#arg0.name+'-address:'+#arg0.getAddress()+'-time:'+#arg0.getBirthday()") User user) { 81 | return false; 82 | } 83 | 84 | /** 85 | * ops 86 | */ 87 | @Override 88 | @Cached(prefix = "total") 89 | public void noParam() { 90 | 91 | } 92 | 93 | @Override 94 | @Cached(prefix = "total") 95 | public void noCacheKey(Object o) { 96 | 97 | } 98 | 99 | @Cached 100 | @Override 101 | public void wrongMultiParam(@CacheKey("#arg0[#i]") Object o) { 102 | 103 | } 104 | 105 | @Cached 106 | @Override 107 | public Map wrongIdentifier(@CacheKey(field = "id") List ids) { 108 | return null; 109 | } 110 | 111 | @Cached 112 | @Override 113 | public List wrongCollectionReturn(@CacheKey(value = "#arg0[#i]") List ids) { 114 | return null; 115 | } 116 | 117 | @Override 118 | @Cached 119 | public List correctIdentifier(@CacheKey(value = "#arg0[#i]", field = "id") List ids) { 120 | List users = new ArrayList<>(2); 121 | for (int id : ids) { 122 | users.add(new User(id, "name" + id, new Date(), id, "zj" + id)); 123 | } 124 | return users; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/cache/TairCache.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.cache; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.jbox.executor.AsyncJobExecutor; 5 | import com.github.jbox.executor.ExecutorManager; 6 | import com.taobao.tair.DataEntry; 7 | import com.taobao.tair.Result; 8 | import com.taobao.tair.ResultCode; 9 | import com.taobao.tair.TairManager; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import javax.annotation.PreDestroy; 13 | import java.io.Serializable; 14 | import java.util.*; 15 | import java.util.concurrent.ExecutorService; 16 | 17 | /** 18 | * @author jifang.zjf 19 | * @since 2017/5/12 下午6:20. 20 | */ 21 | @Slf4j 22 | public class TairCache implements ICache { 23 | 24 | private TairManager tairManager; 25 | 26 | private int namespace; 27 | 28 | private ExecutorService worker; 29 | 30 | public TairCache(TairManager tairManager, int namespace) { 31 | this(tairManager, namespace, ExecutorManager.newFixedThreadPool("CacheXTairWriter", Runtime.getRuntime().availableProcessors())); 32 | } 33 | 34 | public TairCache(TairManager tairManager, int namespace, ExecutorService worker) { 35 | this.namespace = namespace; 36 | this.tairManager = tairManager; 37 | this.worker = worker; 38 | } 39 | 40 | @Override 41 | public Object read(String key) { 42 | Result result = tairManager.get(namespace, key); 43 | DataEntry dataEntry; 44 | if (result.isSuccess() && (dataEntry = result.getValue()) != null) { 45 | return dataEntry.getValue(); 46 | } else { 47 | ResultCode resultCode = result.getRc(); 48 | log.error("tair get error, code: {}, message: {}", resultCode.getCode(), resultCode.getMessage()); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | @Override 55 | public void write(String key, Object value, long expire) { 56 | ResultCode result = tairManager.put(namespace, key, (Serializable) value, 0, (int) (expire / 1000)); 57 | if (!result.isSuccess()) { 58 | log.error("tair put error, code: {}, message: {}", result.getCode(), result.getMessage()); 59 | } 60 | } 61 | 62 | @Override 63 | public Map read(Collection keys) { 64 | Result> results = tairManager.mget(namespace, new ArrayList<>(keys)); 65 | 66 | List entries; 67 | if ((entries = results.getValue()) != null) { 68 | Map resultMap = new HashMap<>(entries.size()); 69 | entries.forEach((entry) -> resultMap.put((String) entry.getKey(), entry.getValue())); 70 | 71 | return resultMap; 72 | } else { 73 | ResultCode resultCode = results.getRc(); 74 | log.error("tair mget error, code: {}, message: {}", resultCode.getCode(), resultCode.getMessage()); 75 | } 76 | 77 | return Collections.emptyMap(); 78 | } 79 | 80 | 81 | // tips: this.tairManager.mput(); X 82 | // 新版本的Tair不再支持mput命令, 因此当key数量过多时建议使用并发方式 Write Tair 83 | @Override 84 | public void write(Map keyValueMap, long expire) { 85 | if (this.worker == null) { 86 | keyValueMap.forEach((key, value) -> this.write(key, value, expire)); 87 | return; 88 | } 89 | 90 | // 并发写入 91 | AsyncJobExecutor job = new AsyncJobExecutor(worker); 92 | keyValueMap.forEach((key, value) -> job.addTask(() -> { 93 | this.write(key, value, expire); 94 | return null; 95 | })); 96 | job.execute().waiting(); 97 | } 98 | 99 | @Override 100 | public void remove(String... keys) { 101 | ResultCode resultCode = tairManager.mdelete(namespace, Arrays.asList(keys)); 102 | if (!resultCode.isSuccess()) { 103 | log.error("tair mdelete error, code: {}, message: {}", resultCode.getCode(), resultCode.getMessage()); 104 | } 105 | } 106 | 107 | @PreDestroy 108 | public void tearDown() { 109 | if (this.tairManager != null) { 110 | this.tairManager.close(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/core/CacheXCore.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.core; 2 | 3 | import com.github.cachex.Cached; 4 | import com.github.cachex.CachedGet; 5 | import com.github.cachex.Invalid; 6 | import com.github.cachex.domain.CacheXAnnoHolder; 7 | import com.github.cachex.domain.CacheXMethodHolder; 8 | import com.github.cachex.domain.Pair; 9 | import com.github.cachex.invoker.Invoker; 10 | import com.github.cachex.manager.CacheManager; 11 | import com.github.cachex.reader.AbstractCacheReader; 12 | import com.github.cachex.utils.CacheXInfoContainer; 13 | import com.github.cachex.utils.CacheXLogger; 14 | import com.github.cachex.utils.KeyGenerator; 15 | import com.google.inject.Inject; 16 | import com.google.inject.Singleton; 17 | import com.google.inject.name.Named; 18 | 19 | import java.lang.reflect.Method; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import static com.github.cachex.utils.SwitcherUtils.isSwitchOn; 24 | 25 | /** 26 | * @author jifang.zjf 27 | * @since 2017/6/26 下午1:03. 28 | */ 29 | @Singleton 30 | public class CacheXCore { 31 | 32 | @Inject 33 | private CacheXConfig config; 34 | 35 | @Inject 36 | private CacheManager cacheManager; 37 | 38 | @Inject 39 | @Named("singleCacheReader") 40 | private AbstractCacheReader singleCacheReader; 41 | 42 | @Inject 43 | @Named("multiCacheReader") 44 | private AbstractCacheReader multiCacheReader; 45 | 46 | public Object read(CachedGet cachedGet, Method method, Invoker invoker) throws Throwable { 47 | Object result; 48 | if (isSwitchOn(config, cachedGet, method, invoker.getArgs())) { 49 | result = doReadWrite(method, invoker, false); 50 | } else { 51 | result = invoker.proceed(); 52 | } 53 | 54 | return result; 55 | } 56 | 57 | public Object readWrite(Cached cached, Method method, Invoker invoker) throws Throwable { 58 | Object result; 59 | if (isSwitchOn(config, cached, method, invoker.getArgs())) { 60 | result = doReadWrite(method, invoker, true); 61 | } else { 62 | result = invoker.proceed(); 63 | } 64 | 65 | return result; 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | public void remove(Invalid invalid, Method method, Object[] args) { 70 | if (isSwitchOn(config, invalid, method, args)) { 71 | 72 | long start = System.currentTimeMillis(); 73 | 74 | CacheXAnnoHolder cacheXAnnoHolder = CacheXInfoContainer.getCacheXInfo(method).getLeft(); 75 | if (cacheXAnnoHolder.isMulti()) { 76 | Map[] pair = KeyGenerator.generateMultiKey(cacheXAnnoHolder, args); 77 | Set keys = ((Map) pair[1]).keySet(); 78 | cacheManager.remove(invalid.value(), keys.toArray(new String[keys.size()])); 79 | 80 | CacheXLogger.info("multi cache clear, keys: {}", keys); 81 | } else { 82 | String key = KeyGenerator.generateSingleKey(cacheXAnnoHolder, args); 83 | cacheManager.remove(invalid.value(), key); 84 | 85 | CacheXLogger.info("single cache clear, key: {}", key); 86 | } 87 | 88 | CacheXLogger.debug("cachex clear total cost [{}] ms", (System.currentTimeMillis() - start)); 89 | } 90 | } 91 | 92 | private Object doReadWrite(Method method, Invoker invoker, boolean needWrite) throws Throwable { 93 | long start = System.currentTimeMillis(); 94 | 95 | Pair pair = CacheXInfoContainer.getCacheXInfo(method); 96 | CacheXAnnoHolder cacheXAnnoHolder = pair.getLeft(); 97 | CacheXMethodHolder cacheXMethodHolder = pair.getRight(); 98 | 99 | Object result; 100 | if (cacheXAnnoHolder.isMulti()) { 101 | result = multiCacheReader.read(cacheXAnnoHolder, cacheXMethodHolder, invoker, needWrite); 102 | } else { 103 | result = singleCacheReader.read(cacheXAnnoHolder, cacheXMethodHolder, invoker, needWrite); 104 | } 105 | 106 | CacheXLogger.debug("cachex read total cost [{}] ms", (System.currentTimeMillis() - start)); 107 | 108 | return result; 109 | } 110 | 111 | public void write() { 112 | // TODO on @CachedPut 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/KeyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.CacheKey; 4 | import com.github.cachex.domain.CacheXAnnoHolder; 5 | 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * @author jifang 11 | * @since 16/7/21 上午11:34. 12 | */ 13 | public class KeyGenerator { 14 | 15 | public static String generateSingleKey(CacheXAnnoHolder cacheXAnnoHolder, Object[] argValues) { 16 | String[] argNames = ArgNameGenerator.getArgNames(cacheXAnnoHolder.getMethod()); 17 | Map cacheKeyMap = cacheXAnnoHolder.getCacheKeyMap(); 18 | String prefix = cacheXAnnoHolder.getPrefix(); 19 | 20 | return doGenerateKey(cacheKeyMap, prefix, argNames, argValues); 21 | } 22 | 23 | //array[]: {multiEntry2Key, key2MultiEntry} 24 | public static Map[] generateMultiKey(CacheXAnnoHolder cacheXAnnoHolder, Object[] argValues) { 25 | /*由于要将Collection内的元素作为Map的Key, 因此就要求元素必须实现的hashcode & equals方法*/ 26 | Map multiEntry2Key = new LinkedHashMap<>(); 27 | Map key2MultiEntry = new LinkedHashMap<>(); 28 | 29 | // -- 准备要拼装key所需的原材料 -- // 30 | // 标记为multi的参数 31 | Collection multiArgEntries = getMultiArgEntries(argValues[cacheXAnnoHolder.getMultiIndex()]); 32 | // 参数索引 -> CacheKey 33 | Map argIndex2CacheKey = cacheXAnnoHolder.getCacheKeyMap(); 34 | // 全局prefix 35 | String prefix = cacheXAnnoHolder.getPrefix(); 36 | 37 | // -- 开始拼装 -- // 38 | 39 | // 根据方法获取原始的参数名 40 | String[] argNames = ArgNameGenerator.getArgNames(cacheXAnnoHolder.getMethod()); 41 | // 给参数名添加一个`#i`遍历指令 42 | String[] appendArgNames = (String[]) appendArray(argNames, "i"); 43 | 44 | int i = 0; 45 | for (Object multiElement : multiArgEntries) { 46 | 47 | // 给参数值数组的`#i`指令赋值 48 | Object[] appendArgValues = appendArray(argValues, i); 49 | 50 | String key = doGenerateKey(argIndex2CacheKey, prefix, appendArgNames, appendArgValues); 51 | 52 | key2MultiEntry.put(key, multiElement); 53 | multiEntry2Key.put(multiElement, key); 54 | ++i; 55 | } 56 | 57 | return new Map[]{multiEntry2Key, key2MultiEntry}; 58 | } 59 | 60 | private static String doGenerateKey(Map parameterIndex2CacheKey, 61 | String prefix, String[] argNames, Object[] argValues) { 62 | 63 | StringBuilder sb = new StringBuilder(prefix); 64 | for (Map.Entry entry : parameterIndex2CacheKey.entrySet()) { 65 | int argIndex = entry.getKey(); 66 | String argSpel = entry.getValue().value(); 67 | 68 | Object defaultValue = getDefaultValue(argValues, argIndex); 69 | Object keyPart = SpelCalculator.calcSpelValueWithContext(argSpel, argNames, argValues, defaultValue); 70 | 71 | sb.append(keyPart); 72 | 73 | } 74 | 75 | return sb.toString(); 76 | } 77 | 78 | /** 79 | * 获取当spel表达式为空(null or '')时, 默认的拼装keyPart 80 | * 注意: 当multi的spel表达式为空时, 这时会将整个`Collection`实例作为keyPart(当然, 这种情况不会发生)... 81 | */ 82 | private static Object getDefaultValue(Object[] argValues, int argIndex) { 83 | return argValues[argIndex]; 84 | } 85 | 86 | /** 87 | * 将标记为`multi`的参数转成`Collection`实例 88 | * 89 | * @param multiArg 90 | * @return 91 | */ 92 | private static Collection getMultiArgEntries(Object multiArg) { 93 | if (multiArg == null) { 94 | return Collections.emptyList(); 95 | } 96 | 97 | if (multiArg instanceof Collection) { 98 | return (Collection) multiArg; 99 | } else if (multiArg instanceof Map) { 100 | return ((Map) multiArg).keySet(); 101 | } else { 102 | // 此处应该在multi参数校验的时候确保只能为Collection、Map、Object[]三种类型 103 | return Arrays.stream((Object[]) multiArg).collect(Collectors.toList()); 104 | } 105 | } 106 | 107 | private static Object[] appendArray(Object[] origin, Object append) { 108 | Object[] dest = Arrays.copyOf(origin, origin.length + 1); 109 | dest[origin.length] = append; 110 | 111 | return dest; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/Addables.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.exception.CacheXException; 4 | import com.github.jbox.utils.Collections3; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author jifang.zjf@alibaba-inc.com (FeiQing) 12 | * @version 1.0 13 | * @since 2018-09-03 12:12:00. 14 | */ 15 | public class Addables { 16 | 17 | public interface Addable { 18 | 19 | Addable init(Class type, int initSize); 20 | 21 | Addable addAll(List list); 22 | 23 | T getResult(); 24 | } 25 | 26 | private static class ArrayAddable implements Addable { 27 | 28 | private Object[] instance; 29 | 30 | @Override 31 | public Addable init(Class type, int initSize) { 32 | this.instance = new Object[initSize]; 33 | return this; 34 | } 35 | 36 | @Override 37 | public Addable addAll(List list) { 38 | for (int i = 0; i < list.size(); ++i) { 39 | this.instance[i] = list.get(i); 40 | } 41 | 42 | return this; 43 | } 44 | 45 | @Override 46 | public Object[] getResult() { 47 | return this.instance; 48 | } 49 | } 50 | 51 | private static class CollectionAddable implements Addable { 52 | 53 | private Collection instance; 54 | 55 | @Override 56 | public Addable init(Class type, int initSize) { 57 | try { 58 | this.instance = type.newInstance(); 59 | } catch (InstantiationException | IllegalAccessException e) { 60 | throw new CacheXException("could not invoke collection: " + type.getName() + "'s no param (default) constructor!", e); 61 | } 62 | 63 | return this; 64 | } 65 | 66 | @Override 67 | public Addable addAll(List list) { 68 | this.instance.addAll(list); 69 | return this; 70 | } 71 | 72 | @Override 73 | public Collection getResult() { 74 | return this.instance; 75 | } 76 | } 77 | 78 | private static class MapAddable implements Addable { 79 | 80 | private Map instance; 81 | 82 | @Override 83 | public Addable init(Class type, int initSize) { 84 | try { 85 | this.instance = type.newInstance(); 86 | } catch (InstantiationException | IllegalAccessException e) { 87 | throw new CacheXException("could not invoke Map: " + type.getName() + "'s no param (default) constructor!", e); 88 | } 89 | 90 | return this; 91 | } 92 | 93 | @Override 94 | public Addable addAll(List list) { 95 | if (Collections3.isEmpty(list)) { 96 | return this; 97 | } 98 | 99 | list.stream().map(obj -> (Map.Entry) obj).forEach(entry -> instance.put(entry.getKey(), entry.getValue())); 100 | 101 | return this; 102 | } 103 | 104 | @Override 105 | public Map getResult() { 106 | return instance; 107 | } 108 | } 109 | 110 | public static Addable newAddable(Class type, int size) { 111 | if (Map.class.isAssignableFrom(type)) { 112 | return new MapAddable().init((Class) type, size); 113 | } else if (Collection.class.isAssignableFrom(type)) { 114 | return new CollectionAddable().init((Class) type, size); 115 | } else { 116 | return new ArrayAddable().init((Class) type, size); 117 | } 118 | } 119 | 120 | public static Collection newCollection(Class type, Collection initCollection) { 121 | try { 122 | Collection collection = (Collection) type.newInstance(); 123 | if (Collections3.isNotEmpty(initCollection)) { 124 | collection.addAll(initCollection); 125 | } 126 | 127 | return collection; 128 | } catch (InstantiationException | IllegalAccessException e) { 129 | throw new CacheXException("could not invoke collection: " + type.getName() + "'s no param (default) constructor!", e); 130 | } 131 | } 132 | 133 | public static Map newMap(Class type, Map initMap) { 134 | try { 135 | Map map = (Map) type.newInstance(); 136 | if (Collections3.isNotEmpty(initMap)) { 137 | map.putAll(initMap); 138 | } 139 | return map; 140 | } catch (InstantiationException | IllegalAccessException e) { 141 | throw new CacheXException("could not invoke map: " + type.getName() + "'s no param (default) constructor!", e); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/MySQLShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import org.springframework.jdbc.core.JdbcOperations; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.function.Supplier; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | import java.util.stream.Stream; 15 | 16 | /** 17 | * @author jifang.zjf 18 | * @since 2017/7/10 下午6:50. 19 | */ 20 | public class MySQLShootingMXBeanImpl extends AbstractDBShootingMXBean { 21 | 22 | private static final String DRIVER_MYSQL = "com.mysql.jdbc.Driver"; 23 | 24 | private static final String URL_MYSQL = "jdbc:mysql://${host}:${port}/${database}"; 25 | 26 | public MySQLShootingMXBeanImpl(String username, String password) { 27 | this("127.0.0.1", 3306, username, password); 28 | } 29 | 30 | public MySQLShootingMXBeanImpl(String host, long port, String username, String password) { 31 | this(host, port, 32 | System.getProperty("product.name", "unnamed"), 33 | username, password); 34 | } 35 | 36 | public MySQLShootingMXBeanImpl(String host, long port, String database, String username, String password) { 37 | super(database, 38 | newHashMap( 39 | "host", host, 40 | "port", port, 41 | "username", username, 42 | "password", password 43 | )); 44 | } 45 | 46 | /** 47 | * could not use. 48 | * 49 | * @param database 50 | */ 51 | private MySQLShootingMXBeanImpl(String database) { 52 | super(database, Collections.emptyMap()); 53 | } 54 | 55 | @Override 56 | protected Supplier jdbcOperationsSupplier(String dbPath, Map context) { 57 | return () -> { 58 | context.put("database", dbPath); 59 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 60 | dataSource.setDriverClassName(DRIVER_MYSQL); 61 | dataSource.setUrl(format(URL_MYSQL, context)); 62 | dataSource.setUsername((String) context.get("username")); 63 | dataSource.setPassword((String) context.get("password")); 64 | 65 | JdbcTemplate template = new JdbcTemplate(dataSource); 66 | template.execute("CREATE TABLE IF NOT EXISTS t_hit_rate(" + 67 | "id BIGINT PRIMARY KEY AUTO_INCREMENT," + 68 | "pattern VARCHAR(64) NOT NULL UNIQUE," + 69 | "hit_count BIGINT NOT NULL DEFAULT 0," + 70 | "require_count BIGINT NOT NULL DEFAULT 0," + 71 | "version BIGINT NOT NULL DEFAULT 0)"); 72 | 73 | return template; 74 | }; 75 | } 76 | 77 | @Override 78 | protected Stream transferResults(List> mapResults) { 79 | return mapResults.stream().map(result -> { 80 | DataDO dataDO = new DataDO(); 81 | dataDO.setRequireCount((Long) result.get("require_count")); 82 | dataDO.setHitCount((Long) result.get("hit_count")); 83 | dataDO.setPattern((String) result.get("pattern")); 84 | dataDO.setVersion((Long) result.get("version")); 85 | 86 | return dataDO; 87 | }); 88 | } 89 | 90 | private static HashMap newHashMap(Object... keyValues) { 91 | HashMap map = new HashMap<>(keyValues.length / 2); 92 | for (int i = 0; i < keyValues.length; i += 2) { 93 | String key = (String) keyValues[i]; 94 | Object value = keyValues[i + 1]; 95 | 96 | map.put(key, value); 97 | } 98 | 99 | return map; 100 | } 101 | 102 | private static final Pattern pattern = Pattern.compile("\\$\\{(\\w)+}"); 103 | 104 | private static String format(String template, Map argMap) { 105 | Matcher matcher = pattern.matcher(template); 106 | 107 | while (matcher.find()) { 108 | String exp = matcher.group(); 109 | Object value = argMap.get(trim(exp)); 110 | String expStrValue = getStringValue(value); 111 | 112 | template = template.replace(exp, expStrValue); 113 | } 114 | 115 | return template; 116 | } 117 | 118 | private static String getStringValue(Object obj) { 119 | String string; 120 | 121 | if (obj instanceof String) { 122 | string = (String) obj; 123 | } else { 124 | string = String.valueOf(obj); 125 | } 126 | 127 | return string; 128 | } 129 | 130 | private static String trim(String string) { 131 | if (string.startsWith("${")) 132 | string = string.substring("${".length()); 133 | 134 | if (string.endsWith("}")) 135 | string = string.substring(0, string.length() - "}".length()); 136 | 137 | return string; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/ArgNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import java.lang.reflect.Method; 4 | import java.lang.reflect.Parameter; 5 | import java.util.Arrays; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ConcurrentMap; 8 | 9 | /** 10 | * @author jifang.zjf 11 | * @since 2017/6/23 上午10:17. 12 | */ 13 | public class ArgNameGenerator { 14 | 15 | private static final String X_ARGS_PREFIX = "args"; 16 | 17 | private static String[] X_ARGS = { 18 | X_ARGS_PREFIX + 0, 19 | X_ARGS_PREFIX + 1, 20 | X_ARGS_PREFIX + 2, 21 | X_ARGS_PREFIX + 3, 22 | X_ARGS_PREFIX + 4, 23 | X_ARGS_PREFIX + 5, 24 | X_ARGS_PREFIX + 6, 25 | X_ARGS_PREFIX + 7, 26 | X_ARGS_PREFIX + 8, 27 | X_ARGS_PREFIX + 9, 28 | X_ARGS_PREFIX + 10, 29 | X_ARGS_PREFIX + 11, 30 | X_ARGS_PREFIX + 12, 31 | X_ARGS_PREFIX + 13, 32 | X_ARGS_PREFIX + 14, 33 | X_ARGS_PREFIX + 15, 34 | X_ARGS_PREFIX + 16, 35 | X_ARGS_PREFIX + 17, 36 | X_ARGS_PREFIX + 18, 37 | X_ARGS_PREFIX + 19 38 | }; 39 | 40 | private static boolean isFirst = true; 41 | 42 | private static final ConcurrentMap methodParameterNames = new ConcurrentHashMap<>(); 43 | 44 | public static String[] getArgNames(Method method) { 45 | return methodParameterNames.computeIfAbsent(method, ArgNameGenerator::doGetArgNamesWithJava8); 46 | } 47 | 48 | // 由于编译参数–parameters的影响, 开启了该参数, 获取到的参数名为真是的方法参数Name; 没有开启: 则是获取到argN这种. 49 | // 为了方便用户, 我们统一生成xArgN这种方式来填充, 同时也兼容原先的这种生成方式¬ 50 | public static String[] getXArgNames(int valueSize) { 51 | if (valueSize == 0) { 52 | return new String[0]; 53 | } 54 | 55 | String[] xArgs = new String[valueSize]; 56 | for (int i = 0; i < valueSize; ++i) { 57 | xArgs[i] = i < X_ARGS.length ? X_ARGS[i] : X_ARGS_PREFIX + i; 58 | } 59 | 60 | return xArgs; 61 | } 62 | 63 | // Java1.8之后提供了获取参数名方法, 但需要编译时添加`–parameters`参数支持, 如`javac –parameters`, 不然参数名为'arg0' 64 | private static String[] doGetArgNamesWithJava8(Method method) { 65 | Parameter[] parameters = method.getParameters(); 66 | String[] argNames = Arrays.stream(parameters).map(Parameter::getName).toArray(String[]::new); 67 | if (isFirst && argNames.length != 0 && argNames[0].equals("arg0")) { 68 | CacheXLogger.warn("compile not set '–parameters', used default method parameter names"); 69 | isFirst = false; 70 | } 71 | 72 | return argNames; 73 | } 74 | 75 | /* 76 | private static class SpringInnerClass { 77 | 78 | private static LocalVariableTableParameterNameDiscoverer discoverer = 79 | new LocalVariableTableParameterNameDiscoverer(); 80 | 81 | private static String[] doGetParameterNames(Method method) { 82 | return discoverer.getParameterNames(method); 83 | } 84 | } 85 | 86 | private static class InnerJavassistClass { 87 | 88 | private static final ClassPool pool; 89 | 90 | static { 91 | pool = ClassPool.getDefault(); 92 | // 自定义ClassLoader情况 93 | pool.insertClassPath(new ClassClassPath(ArgNameSupplier.class)); 94 | } 95 | 96 | private static String[] doGetParameterNames(Method method) { 97 | 98 | CtMethod ctMethod = getCtMethod(method); 99 | 100 | // 1. 拿到方法局部变量表 101 | LocalVariableAttribute localVariableAttribute = (LocalVariableAttribute) ctMethod 102 | .getMethodInfo2() 103 | .getCodeAttribute() 104 | .getAttribute(LocalVariableAttribute.tag); 105 | 106 | // 2. 组织为有序map(以变量在表中所处的slots位置排序) 107 | // 详见: http://www.cnblogs.com/hucn/p/3636912.html 108 | SortedMap variableMap = new TreeMap<>(); 109 | for (int i = 0; i < localVariableAttribute.tableLength(); i++) { 110 | variableMap.put(localVariableAttribute.index(i), localVariableAttribute.variableName(i)); 111 | } 112 | 113 | // 3. 将有序的参数名列表导出到array 114 | Collection parameterNames = variableMap.values(); 115 | int offset = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1; 116 | 117 | return Arrays.copyOfRange(parameterNames.toArray(), offset, offset + method.getParameterCount(), String[].class); 118 | } 119 | 120 | private static CtMethod getCtMethod(Method method) { 121 | 122 | try { 123 | // 方法所属的ctClass 124 | CtClass methodClass = getCtClass(method.getDeclaringClass()); 125 | 126 | // 方法参数所属的ctClass 127 | Class[] parameterTypes = method.getParameterTypes(); 128 | CtClass[] parameterClass = new CtClass[parameterTypes.length]; 129 | for (int i = 0; i < parameterTypes.length; ++i) { 130 | parameterClass[i] = getCtClass(parameterTypes[i]); 131 | } 132 | 133 | return methodClass.getDeclaredMethod(method.getName(), parameterClass); 134 | } catch (NotFoundException e) { 135 | throw new CacheXException(e); 136 | } 137 | } 138 | 139 | private static CtClass getCtClass(Class clazz) throws NotFoundException { 140 | return InnerJavassistClass.pool.getCtClass(clazz.getName()); 141 | } 142 | } 143 | */ 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/DerbyShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import com.github.cachex.exception.CacheXException; 4 | import com.google.common.base.Splitter; 5 | import com.google.common.base.Strings; 6 | import org.springframework.jdbc.core.JdbcOperations; 7 | import org.springframework.jdbc.core.JdbcTemplate; 8 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 9 | 10 | import javax.annotation.PreDestroy; 11 | import javax.sql.DataSource; 12 | import java.io.File; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.lang.reflect.Method; 15 | import java.net.MalformedURLException; 16 | import java.net.URL; 17 | import java.net.URLClassLoader; 18 | import java.sql.SQLException; 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.function.Supplier; 23 | import java.util.stream.Stream; 24 | 25 | /** 26 | * @author jifang.zjf 27 | * @since 2017/6/9 下午12:35. 28 | */ 29 | public class DerbyShootingMXBeanImpl extends AbstractDBShootingMXBean { 30 | 31 | private static final String DERBY_JAR_REGEX = ".*/derby-([\\d\\.]+)\\.jar"; 32 | 33 | private static final String DERBY_JAR_PATH = "%s/../db/lib/derby.jar"; 34 | 35 | private static final String[] classPaths = { 36 | "sun.boot.class.path", 37 | "java.ext.dirs", 38 | "java.class.path" 39 | }; 40 | 41 | public DerbyShootingMXBeanImpl() { 42 | this(System.getProperty("user.home") + "/.Derby"); 43 | } 44 | 45 | public DerbyShootingMXBeanImpl(String derbyFilePath) { 46 | super(derbyFilePath, Collections.emptyMap()); 47 | } 48 | 49 | @Override 50 | protected Supplier jdbcOperationsSupplier(String dbPath, Map context) { 51 | return () -> { 52 | registerDerbyDriver(); 53 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 54 | dataSource.setUrl(String.format("jdbc:derby:%s;create=true", dbPath)); 55 | JdbcOperations jdbcOperations = new JdbcTemplate(dataSource); 56 | try { 57 | if (isTableNotExists(dataSource)) { 58 | jdbcOperations.execute("CREATE TABLE T_HIT_RATE (" + 59 | "id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY," + 60 | "pattern VARCHAR(64) NOT NULL UNIQUE," + 61 | "hit_count BIGINT NOT NULL DEFAULT 0," + 62 | "require_count BIGINT NOT NULL DEFAULT 0," + 63 | "version BIGINT NOT NULL DEFAULT 0)"); 64 | } 65 | } catch (SQLException e) { 66 | throw new CacheXException("derby create table: T_HIT_RATE error", e); 67 | } 68 | 69 | return jdbcOperations; 70 | }; 71 | } 72 | 73 | private boolean isTableNotExists(DataSource dataSource) throws SQLException { 74 | return !dataSource 75 | .getConnection() 76 | .getMetaData() 77 | .getTables(null, null, "T_HIT_RATE", new String[]{"TABLE"}) 78 | .next(); 79 | } 80 | 81 | @Override 82 | protected Stream transferResults(List> mapResults) { 83 | return mapResults.stream().map((map) -> { 84 | DataDO data = new DataDO(); 85 | data.setPattern((String) map.get("PATTERN")); 86 | data.setHitCount((long) map.get("HIT_COUNT")); 87 | data.setRequireCount((long) map.get("REQUIRE_COUNT")); 88 | data.setVersion((long) map.get("VERSION")); 89 | return data; 90 | }); 91 | } 92 | 93 | // --------------------- // 94 | // ---- Derby Driver --- // 95 | // --------------------- // 96 | private void registerDerbyDriver() { 97 | if (!containsDerbyJar()) { 98 | String jarFilePath = String.format(DERBY_JAR_PATH, System.getProperty("java.home")); 99 | if (new File(jarFilePath).exists()) { 100 | loadJar(jarFilePath); 101 | } 102 | } 103 | 104 | try { 105 | Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); 106 | } catch (ClassNotFoundException e) { 107 | throw new CacheXException("derby.jar is not in classpath and not in ${JAVA_HOME}/db/lib directory, " 108 | + "please make sure derby's drive has loaded in classpath", e); 109 | } 110 | } 111 | 112 | private boolean containsDerbyJar() { 113 | 114 | boolean contains = false; 115 | for (int i = 0; !contains && i < classPaths.length; ++i) { 116 | String jarStr; 117 | if (!Strings.isNullOrEmpty(jarStr = System.getProperty(classPaths[i]))) { 118 | for (String jar : Splitter.on(":").split(jarStr)) { 119 | if (jar.matches(DERBY_JAR_REGEX)) { 120 | contains = true; 121 | break; 122 | } 123 | } 124 | } 125 | } 126 | 127 | return contains; 128 | } 129 | 130 | private void loadJar(String javaFile) { 131 | URLClassLoader loader = (URLClassLoader) ClassLoader.getSystemClassLoader(); 132 | 133 | Method addURLMethod = null; 134 | try { 135 | addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); 136 | } catch (NoSuchMethodException ignored) { 137 | } 138 | assert addURLMethod != null; 139 | addURLMethod.setAccessible(true); 140 | try { 141 | addURLMethod.invoke(loader, new URL("file://" + javaFile)); 142 | } catch (IllegalAccessException | InvocationTargetException | MalformedURLException ignored) { 143 | } 144 | } 145 | 146 | @PreDestroy 147 | public void tearDown() { 148 | super.tearDown(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/manager/CacheManager.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.manager; 2 | 3 | import com.github.cachex.ICache; 4 | import com.github.cachex.domain.CacheReadResult; 5 | import com.github.cachex.domain.Pair; 6 | import com.github.cachex.exception.CacheXException; 7 | import com.github.cachex.utils.CacheXLogger; 8 | import com.google.common.base.Strings; 9 | import com.google.inject.Inject; 10 | import com.google.inject.Singleton; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.*; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | /** 18 | * @author jifang 19 | * @since 16/7/7. 20 | */ 21 | @Singleton 22 | public class CacheManager { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(CacheManager.class); 25 | 26 | // defaultCache和cachePool直接使用Pair实现, 减小new Object的损耗 27 | private Pair defaultCache; 28 | 29 | private Map> cachePool = new ConcurrentHashMap<>(); 30 | 31 | @Inject 32 | public void setCachePool(Map caches) { 33 | // default cache impl 34 | Map.Entry entry = caches.entrySet().iterator().next(); 35 | this.defaultCache = Pair.of(entry.getKey(), entry.getValue()); 36 | 37 | caches.forEach((name, cache) -> this.cachePool.put(name, Pair.of(name, cache))); 38 | } 39 | 40 | public Object readSingle(String cache, String key) { 41 | try { 42 | Pair cacheImpl = getCacheImpl(cache); 43 | 44 | long start = System.currentTimeMillis(); 45 | Object result = cacheImpl.getRight().read(key); 46 | CacheXLogger.info("cache [{}] read single cost: [{}] ms", 47 | cacheImpl.getLeft(), 48 | (System.currentTimeMillis() - start)); 49 | 50 | return result; 51 | } catch (Throwable e) { 52 | logger.error("read single cache failed, key: {} ", key, e); 53 | CacheXLogger.error("read single cache failed, key: {} ", key, e); 54 | return null; 55 | } 56 | } 57 | 58 | public void writeSingle(String cache, String key, Object value, int expire) { 59 | if (value != null) { 60 | try { 61 | Pair cacheImpl = getCacheImpl(cache); 62 | 63 | long start = System.currentTimeMillis(); 64 | cacheImpl.getRight().write(key, value, expire); 65 | CacheXLogger.info("cache [{}] write single cost: [{}] ms", 66 | cacheImpl.getLeft(), 67 | (System.currentTimeMillis() - start)); 68 | 69 | } catch (Throwable e) { 70 | logger.error("write single cache failed, key: {} ", key, e); 71 | CacheXLogger.error("write single cache failed, key: {} ", key, e); 72 | } 73 | } 74 | } 75 | 76 | public CacheReadResult readBatch(String cache, Collection keys) { 77 | CacheReadResult cacheReadResult; 78 | if (keys.isEmpty()) { 79 | cacheReadResult = new CacheReadResult(); 80 | } else { 81 | try { 82 | Pair cacheImpl = getCacheImpl(cache); 83 | 84 | long start = System.currentTimeMillis(); 85 | Map cacheMap = cacheImpl.getRight().read(keys); 86 | CacheXLogger.info("cache [{}] read batch cost: [{}] ms", 87 | cacheImpl.getLeft(), 88 | (System.currentTimeMillis() - start)); 89 | 90 | // collect not nit keys, keep order when full shooting 91 | Map hitValueMap = new LinkedHashMap<>(); 92 | Set notHitKeys = new LinkedHashSet<>(); 93 | for (String key : keys) { 94 | Object value = cacheMap.get(key); 95 | 96 | if (value == null) { 97 | notHitKeys.add(key); 98 | } else { 99 | hitValueMap.put(key, value); 100 | } 101 | } 102 | 103 | cacheReadResult = new CacheReadResult(hitValueMap, notHitKeys); 104 | } catch (Throwable e) { 105 | logger.error("read multi cache failed, keys: {}", keys, e); 106 | CacheXLogger.error("read multi cache failed, keys: {}", keys, e); 107 | cacheReadResult = new CacheReadResult(); 108 | } 109 | } 110 | 111 | return cacheReadResult; 112 | } 113 | 114 | public void writeBatch(String cache, Map keyValueMap, int expire) { 115 | try { 116 | Pair cacheImpl = getCacheImpl(cache); 117 | 118 | long start = System.currentTimeMillis(); 119 | cacheImpl.getRight().write(keyValueMap, expire); 120 | CacheXLogger.info("cache [{}] write batch cost: [{}] ms", 121 | cacheImpl.getLeft(), 122 | (System.currentTimeMillis() - start)); 123 | 124 | } catch (Exception e) { 125 | logger.error("write map multi cache failed, keys: {}", keyValueMap.keySet(), e); 126 | CacheXLogger.error("write map multi cache failed, keys: {}", keyValueMap.keySet(), e); 127 | } 128 | } 129 | 130 | public void remove(String cache, String... keys) { 131 | if (keys != null && keys.length != 0) { 132 | try { 133 | Pair cacheImpl = getCacheImpl(cache); 134 | 135 | long start = System.currentTimeMillis(); 136 | cacheImpl.getRight().remove(keys); 137 | CacheXLogger.info("cache [{}] remove cost: [{}] ms", 138 | cacheImpl.getLeft(), 139 | (System.currentTimeMillis() - start)); 140 | 141 | } catch (Throwable e) { 142 | logger.error("remove cache failed, keys: {}: ", keys, e); 143 | CacheXLogger.error("remove cache failed, keys: {}: ", keys, e); 144 | } 145 | } 146 | } 147 | 148 | private Pair getCacheImpl(String cacheName) { 149 | if (Strings.isNullOrEmpty(cacheName)) { 150 | return defaultCache; 151 | } else { 152 | return cachePool.computeIfAbsent(cacheName, (key) -> { 153 | throw new CacheXException(String.format("no cache implementation named [%s].", key)); 154 | }); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CacheX 注解缓存框架 2 | > 1.7.3-Hessian-SNAPSHOT 3 | 4 | - 目前已接入10+应用, 欢迎同学们使用并提出宝贵建议~~ 5 | 6 | --- 7 | 8 | ## I. 架构 9 | ![架构模型](https://img.alicdn.com/tfs/TB1KPtTusfpK1RjSZFOXXa6nFXa-756-731.png) 10 | 11 | --- 12 | ## II. 简单使用 13 | ### 配置 14 | - pom 15 | ```xml 16 | 17 | com.github.cachex 18 | cachex 19 | ${cachex.version} 20 | 21 | ``` 22 | - XML注册 23 | ```xml 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | --- 55 | 56 | ### 使用 57 | #### 1. 添加缓存(`@Cached` & `@CacheKey`) 58 | - 在要添加缓存的方法上标`@Cached` 59 | - 在要组装为key的方法参数上标`@CacheKey` 60 | 61 | ![](https://img.alicdn.com/tfs/TB1QQd4n26TBKNjSZJiXXbKVFXa-626-144.png) 62 | 63 | --- 64 | #### 2. 缓存失效(`@Invalid` & `@CacheKey`) 65 | ![](https://img.alicdn.com/tfs/TB1FyI2n5AnBKNjSZFvXXaTKXXa-631-111.png) 66 | 67 | --- 68 | ## III. 注解详解 69 | > CacheX提供如下注解`@Cached`、`@Invalid`、`@CacheKey`. 70 | (ext: `@CachedGet`、`@CachedWrite`) 71 | 72 | --- 73 | ### @Cached 74 | - 在需要走缓存的方法前添加`@Cached`注解. 75 | 76 | ```java 77 | /** 78 | * @author jifang 79 | * @since 2016/11/2 下午2:22. 80 | */ 81 | @Documented 82 | @Target(value = ElementType.METHOD) 83 | @Retention(RetentionPolicy.RUNTIME) 84 | public @interface Cached { 85 | 86 | /** 87 | * @return Specifies the Used cache implementation, 88 | * default the first {@code caches} config in {@code CacheXAspect} 89 | */ 90 | String value() default ""; 91 | 92 | /** 93 | * @return Specifies the start prefix on every key, 94 | * if the {@code Method} have non {@code param}, 95 | * {@code prefix} is the constant key used by this {@code Method} 96 | */ 97 | String prefix() default ""; 98 | 99 | /** 100 | * @return use SpEL, 101 | * when this spel is {@code true}, this {@code Method} will go through by cache 102 | */ 103 | String condition() default ""; 104 | 105 | /** 106 | * @return expire time, time unit: seconds 107 | */ 108 | int expire() default Expire.FOREVER; 109 | } 110 | ``` 111 | 112 | | 属性 | 描述 | Ext | 113 | :-------: | ------- | ------- 114 | | `value` | 指定缓存实现: `CacheXAspect`/`CacheXProxy`的`caches`参数的key | 选填: 默认为注入caches的第一个实现(即`caches`的第一个Entry实例) | 115 | | `prefix` | 缓存**key**的统一前缀 | 选填: 默认为`""`, 若方法无参或没有`@CacheKey`注解, 则必须在此配置一个`prefix`, 令其成为***缓存静态常量key*** | 116 | | `condition` | SpEL表达式 | 选填: 默认为`""`(`true`), 在CacheX执行前会先eval该表达式, 当表达式值为`true`才会执行缓存逻辑 | 117 | | `expire` | 缓存过期时间(秒) | 选填: 默认为`Expire.FOREVER` | 118 | 119 | 120 | --- 121 | 122 | ### @Invalid 123 | - 在需要失效缓存的方法前添加`@Invalid`注解. 124 | 125 | ```java 126 | @Documented 127 | @Target(value = ElementType.METHOD) 128 | @Retention(RetentionPolicy.RUNTIME) 129 | public @interface Invalid { 130 | 131 | /** 132 | * @return as {@code @Cached} 133 | * @since 0.3 134 | */ 135 | String value() default ""; 136 | 137 | /** 138 | * @return as {@code @Cached} 139 | * @since 0.3 140 | */ 141 | String prefix() default ""; 142 | 143 | /** 144 | * @return as {@code @Cached} 145 | * @since 0.3 146 | */ 147 | String condition() default ""; 148 | } 149 | ``` 150 | > 注解内属性含义与`@Cached`相同. 151 | 152 | --- 153 | 154 | ### @CacheKey 155 | - 在需要作为缓存key的方法参数前添加`@CacheKey`注解. 156 | 157 | ```java 158 | @Documented 159 | @Target(ElementType.PARAMETER) 160 | @Retention(RetentionPolicy.RUNTIME) 161 | public @interface CacheKey { 162 | 163 | /** 164 | * @return use a part of param as a cache key part 165 | */ 166 | String value() default ""; 167 | 168 | /** 169 | * @return used multi model(value has `#i` index) and method return {@code Collection}, 170 | * the {@code field} indicate which of the {@code Collection}'s entity field related with this param 171 | */ 172 | String field() default ""; 173 | } 174 | ``` 175 | 176 | | 属性 | 描述 | Ext | 177 | :-------: | ------- | ------- 178 | | `value` | SpEL表达式: 缓存key的拼装逻辑 | 选填: 默认为`""` | 179 | | `field` | **批量模式**(`value`参数包含`#i`索引)且方法返回值为`Collection`时生效: 指明该返回值的某个属性是与该参数是关联起来的 | 详见Ext.批量模式 | 180 | 181 | 182 | --- 183 | 184 | ### Ext. @CachedGet 185 | - 在需要走缓存的方法前添加`@CachedGet`注解. 186 | > 与`@Cached`的不同在于`@CachedGet`只会从缓存内查询, 不会写入缓存(当缓存不存在时, 只是会取执行方法, 但不讲方法返回内容写入缓存). 187 | 188 | ```java 189 | @Documented 190 | @Target(value = ElementType.METHOD) 191 | @Retention(RetentionPolicy.RUNTIME) 192 | public @interface CachedGet { 193 | 194 | /** 195 | * @return Specifies the Used cache implementation, 196 | * default the first {@code caches} config in {@code CacheXAspect} 197 | * @since 0.3 198 | */ 199 | String value() default ""; 200 | 201 | /** 202 | * @return Specifies the start keyExp on every key, 203 | * if the {@code Method} have non {@code param}, 204 | * {@code keyExp} is the constant key used by this {@code Method} 205 | * @since 0.3 206 | */ 207 | String prefix() default ""; 208 | 209 | /** 210 | * @return use SpEL, 211 | * when this spel is {@code true}, this {@Code Method} will go through by cache 212 | * @since 0.3 213 | */ 214 | String condition() default ""; 215 | } 216 | ``` 217 | 218 | > 注解内属性含义与`@Cached`相同. 219 | 220 | --- 221 | 222 | ### Ext. @CachedWrite 223 | - 在需要写缓存的方法前添加`@CachedGet`注解. 224 | > 与`@Cached`的不同在于`@CachedWrite`只会将方法执行返回值写入缓存, 但不会从缓存中加载数据(todo: 待实现). 225 | 226 | --- 227 | ### Ext. 批量模式 228 | 229 | ![](https://img.alicdn.com/tfs/TB16uFgu8jTBKNjSZFNXXasFXXa-1042-133.png) 230 | 231 | 在该模式下: `#i`指定了ids作为批量参数: 假设ids={1,2,3}, CacheX会结合前面的prefix组装出 {`[USER]:1`、`[USER]:2`、`[USER]:3`} 这3个key去批量的查询缓存, 假设只有{1,2}能够命中, 则CacheX会只保留{3}去调用`getUsers()`方法, 将返回值写入缓存后, 将两部分内容进行merge返回. 232 | 233 | 1. 注意1: 如果方法的返回值为`Collection`实例: 则`@CacheKey`必须指定`field`参数, 该参数会指定`Collection`元素(如`User`)内的某个属性(如`id`)与批量参数的元素(如`ids`内的元素项)是一一对应的, 这样CacheX就可以根据该属性提取出参数值, 拼装key然后写入缓存. 234 | 2. 注意2. 如果方法的返回值为`Map`实例: 则`field`属性不填, 默认使用Map的Key作为`field`. 235 | 3. 注意3. `#i`作为批量模式指示器, 批量模式需要使用`#i`来开启, `#i`指明某个参数作为批量参数, CacheX会不断的迭代该参数生成批量缓存key进行缓存的读写. 236 | 237 | --- 238 | ### Ext. SpEL执行环境 239 | 对于`@CacheKey`内的`value`属性(SpEL), CacheX在将方法的参数组装为key时, 会将整个方法的参数导入到SpEL的执行环境内, 240 | 所以在任一参数的`@CacheKey`的`value`属性内都可以自由的引用这些变量, 如: 241 | ![](https://img.alicdn.com/tfs/TB1Mza0pcj_B1NjSZFHXXaDWpXa-1039-529.png) 242 | 尽管在`arg0`我们可以引用整个方法的任意参数, 但为了可读性, 我们仍然建议对某个参数的引用放在该参数自己的`@CacheKey`内 243 | ![](https://img.alicdn.com/tfs/TB1U23qn7omBKNjSZFqXXXtqVXa-1206-440.png) 244 | 245 | > 注意: 在Java8环境中, 如果编译时没有指定`-parameters`参数, 则参数名默认为`arg0`、`arg1`、...、`argN`, 如果指定了该参数, 则在`spel`中使用实际的参数名即可, 如:`#source.name()`; 为了兼容这两种方式, CacheX提供了自己的命名方式`args0`、`args1`、...、`argsN`, 使用户可以不用区分是否开启编译参数. 246 | 247 | ### Ext. CacheX当前默认支持的缓存实现 248 | ![](https://img.alicdn.com/tfs/TB1JF82uwHqK1RjSZJnXXbNLpXa-259-231.png) 249 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/reader/MultiCacheReader.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.reader; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | import com.github.cachex.core.CacheXConfig; 5 | import com.github.cachex.domain.CacheReadResult; 6 | import com.github.cachex.domain.CacheXAnnoHolder; 7 | import com.github.cachex.domain.CacheXMethodHolder; 8 | import com.github.cachex.invoker.Invoker; 9 | import com.github.cachex.manager.CacheManager; 10 | import com.github.cachex.utils.PatternGenerator; 11 | import com.github.cachex.utils.*; 12 | import com.google.inject.Inject; 13 | import com.google.inject.Singleton; 14 | 15 | import java.util.*; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * @author jifang 20 | * @since 2016/11/5 下午3:11. 21 | */ 22 | @Singleton 23 | public class MultiCacheReader extends AbstractCacheReader { 24 | 25 | @Inject 26 | private CacheManager cacheManager; 27 | 28 | @Inject 29 | private CacheXConfig config; 30 | 31 | @Inject(optional = true) 32 | private ShootingMXBean shootingMXBean; 33 | 34 | @Override 35 | public Object read(CacheXAnnoHolder cacheXAnnoHolder, CacheXMethodHolder cacheXMethodHolder, Invoker invoker, boolean needWrite) throws Throwable { 36 | // compose keys 37 | Map[] pair = KeyGenerator.generateMultiKey(cacheXAnnoHolder, invoker.getArgs()); 38 | Map key2MultiEntry = pair[1]; 39 | 40 | // request cache 41 | Set keys = key2MultiEntry.keySet(); 42 | CacheReadResult cacheReadResult = cacheManager.readBatch(cacheXAnnoHolder.getCache(), keys); 43 | doRecord(cacheReadResult, cacheXAnnoHolder); 44 | 45 | Object result; 46 | // have miss keys : part hit || all not hit 47 | if (!cacheReadResult.getMissKeySet().isEmpty()) { 48 | result = handlePartHit(invoker, cacheReadResult, cacheXAnnoHolder, cacheXMethodHolder, pair, needWrite); 49 | } 50 | // no miss keys : all hit || empty key 51 | else { 52 | Map keyValueMap = cacheReadResult.getHitKeyMap(); 53 | result = handleFullHit(invoker, keyValueMap, cacheXMethodHolder, key2MultiEntry); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | private Object handlePartHit(Invoker invoker, CacheReadResult cacheReadResult, 60 | CacheXAnnoHolder cacheXAnnoHolder, CacheXMethodHolder cacheXMethodHolder, 61 | Map[] pair, boolean needWrite) throws Throwable { 62 | 63 | Map multiEntry2Key = pair[0]; 64 | Map key2MultiEntry = pair[1]; 65 | 66 | Set missKeys = cacheReadResult.getMissKeySet(); 67 | Map hitKeyValueMap = cacheReadResult.getHitKeyMap(); 68 | 69 | // 用未命中的keys调用方法 70 | Object[] missArgs = toMissArgs(missKeys, key2MultiEntry, invoker.getArgs(), cacheXAnnoHolder.getMultiIndex()); 71 | Object proceed = doLogInvoke(() -> invoker.proceed(missArgs)); 72 | 73 | Object result; 74 | if (proceed != null) { 75 | Class returnType = proceed.getClass(); 76 | cacheXMethodHolder.setReturnType(returnType); 77 | if (Map.class.isAssignableFrom(returnType)) { 78 | Map proceedEntryValueMap = (Map) proceed; 79 | 80 | // @since 1.5.4 为了兼容@CachedGet注解, 客户端缓存 81 | if (needWrite) { 82 | // 将方法调用返回的map转换成key_value_map写入Cache 83 | Map keyValueMap = KeyValueUtils.mapToKeyValue(proceedEntryValueMap, missKeys, multiEntry2Key, config.getPrevent()); 84 | cacheManager.writeBatch(cacheXAnnoHolder.getCache(), keyValueMap, cacheXAnnoHolder.getExpire()); 85 | } 86 | // 将方法调用返回的map与从Cache中读取的key_value_map合并返回 87 | result = ResultUtils.mergeMap(returnType, proceedEntryValueMap, key2MultiEntry, hitKeyValueMap); 88 | } else { 89 | Collection proceedCollection = asCollection(proceed, returnType); 90 | 91 | // @since 1.5.4 为了兼容@CachedGet注解, 客户端缓存 92 | if (needWrite) { 93 | // 将方法调用返回的collection转换成key_value_map写入Cache 94 | Map keyValueMap = KeyValueUtils.collectionToKeyValue(proceedCollection, cacheXAnnoHolder.getId(), missKeys, multiEntry2Key, config.getPrevent()); 95 | cacheManager.writeBatch(cacheXAnnoHolder.getCache(), keyValueMap, cacheXAnnoHolder.getExpire()); 96 | } 97 | // 将方法调用返回的collection与从Cache中读取的key_value_map合并返回 98 | Collection resultCollection = ResultUtils.mergeCollection(returnType, proceedCollection, hitKeyValueMap); 99 | result = asType(resultCollection, returnType); 100 | } 101 | } else { 102 | // read as full shooting 103 | result = handleFullHit(invoker, hitKeyValueMap, cacheXMethodHolder, key2MultiEntry); 104 | } 105 | 106 | return result; 107 | } 108 | 109 | private Object asType(Collection collection, Class returnType) { 110 | if (Collection.class.isAssignableFrom(returnType)) { 111 | return collection; 112 | } 113 | 114 | return collection.toArray(); 115 | } 116 | 117 | private Collection asCollection(Object proceed, Class returnType) { 118 | if (Collection.class.isAssignableFrom(returnType)) { 119 | return (Collection) proceed; 120 | } 121 | 122 | return Arrays.asList((Object[]) proceed); 123 | } 124 | 125 | private Object handleFullHit(Invoker invoker, Map keyValueMap, 126 | CacheXMethodHolder cacheXMethodHolder, Map key2Id) throws Throwable { 127 | 128 | Object result; 129 | Class returnType = cacheXMethodHolder.getReturnType(); 130 | 131 | // when method return type not cached. case: full shooting when application restart 132 | if (returnType == null) { 133 | result = doLogInvoke(invoker::proceed); 134 | 135 | // catch return type for next time 136 | if (result != null) { 137 | cacheXMethodHolder.setReturnType(result.getClass()); 138 | } 139 | } else { 140 | if (cacheXMethodHolder.isCollection()) { 141 | result = ResultUtils.toCollection(returnType, keyValueMap); 142 | } else { 143 | result = ResultUtils.toMap(returnType, key2Id, keyValueMap); 144 | } 145 | } 146 | 147 | return result; 148 | } 149 | 150 | private Object[] toMissArgs(Set missKeys, Map keyIdMap, 151 | Object[] args, int multiIndex) { 152 | 153 | List missedMultiEntries = missKeys.stream() 154 | .map(keyIdMap::get) 155 | .collect(Collectors.toList()); 156 | 157 | Class multiArgType = args[multiIndex].getClass(); 158 | 159 | // 对将Map作为CacheKey的支持就到这儿了, 不会再继续下去... 160 | Addables.Addable addable = Addables.newAddable(multiArgType, missedMultiEntries.size()); 161 | args[multiIndex] = addable.addAll(missedMultiEntries).getResult(); 162 | 163 | return args; 164 | } 165 | 166 | private void doRecord(CacheReadResult cacheReadResult, CacheXAnnoHolder cacheXAnnoHolder) { 167 | Set missKeys = cacheReadResult.getMissKeySet(); 168 | 169 | // 计数 170 | int hitCount = cacheReadResult.getHitKeyMap().size(); 171 | int totalCount = hitCount + missKeys.size(); 172 | CacheXLogger.info("multi cache hit rate: {}/{}, missed keys: {}", 173 | hitCount, totalCount, missKeys); 174 | 175 | if (this.shootingMXBean != null) { 176 | // 分组模板 177 | String pattern = PatternGenerator.generatePattern(cacheXAnnoHolder); 178 | 179 | this.shootingMXBean.hitIncr(pattern, hitCount); 180 | this.shootingMXBean.reqIncr(pattern, totalCount); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/ZKShootingMXBeanImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | import com.github.cachex.domain.Pair; 5 | import com.github.cachex.exception.CacheXException; 6 | import org.apache.curator.framework.CuratorFramework; 7 | import org.apache.curator.framework.CuratorFrameworkFactory; 8 | import org.apache.curator.framework.recipes.atomic.AtomicValue; 9 | import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong; 10 | import org.apache.curator.retry.RetryNTimes; 11 | import org.apache.zookeeper.KeeperException; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import javax.annotation.PreDestroy; 16 | import java.util.HashMap; 17 | import java.util.LinkedHashMap; 18 | import java.util.Map; 19 | import java.util.concurrent.*; 20 | import java.util.concurrent.atomic.AtomicLong; 21 | 22 | /** 23 | * @author jifang.zjf 24 | * @since 2017/6/14 下午4:35. 25 | */ 26 | public class ZKShootingMXBeanImpl implements ShootingMXBean { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(ZKShootingMXBeanImpl.class); 29 | 30 | private static final ExecutorService executor = Executors.newSingleThreadExecutor(r -> { 31 | Thread thread = new Thread(r); 32 | thread.setName("cachex:zk-shooting-uploader"); 33 | thread.setDaemon(true); 34 | return thread; 35 | }); 36 | 37 | private static final String NAME_SPACE = "cachex"; 38 | 39 | private volatile boolean isShutdown = false; 40 | 41 | private BlockingQueue> hitQueue = new LinkedTransferQueue<>(); 42 | 43 | private BlockingQueue> requireQueue = new LinkedTransferQueue<>(); 44 | 45 | private Map hitCounterMap = new HashMap<>(); 46 | 47 | private Map requireCounterMap = new HashMap<>(); 48 | 49 | private CuratorFramework client; 50 | 51 | private String hitPathPrefix; 52 | 53 | private String requirePathPrefix; 54 | 55 | public ZKShootingMXBeanImpl(String zkServers) { 56 | this(zkServers, System.getProperty("product.name", "unnamed")); 57 | } 58 | 59 | public ZKShootingMXBeanImpl(String zkServers, String productName) { 60 | this.client = CuratorFrameworkFactory.builder() 61 | .connectString(zkServers) 62 | .retryPolicy(new RetryNTimes(3, 0)) 63 | .namespace(NAME_SPACE) 64 | .build(); 65 | client.start(); 66 | 67 | // append prefix and suffix 68 | String uniqueProductName = processProductName(productName); 69 | this.hitPathPrefix = String.format("%s%s", uniqueProductName, "hit"); 70 | this.requirePathPrefix = String.format("%s%s", uniqueProductName, "require"); 71 | try { 72 | client.create().creatingParentsIfNeeded().forPath(hitPathPrefix); 73 | client.create().creatingParentsIfNeeded().forPath(requirePathPrefix); 74 | LOGGER.info("create path:[{}],[{}] on namespace: [{}] success", hitPathPrefix, requirePathPrefix, NAME_SPACE); 75 | } catch (KeeperException.NodeExistsException ignored) { 76 | LOGGER.warn("path: [{}], [{}] on namespace: [{}] is exits, please make product name:{} is unique", hitPathPrefix, requirePathPrefix, NAME_SPACE, productName); 77 | } catch (Exception e) { 78 | throw new CacheXException("create path: " + hitPathPrefix + ", " + requirePathPrefix + " on namespace: " + NAME_SPACE + " error", e); 79 | } 80 | 81 | executor.submit(() -> { 82 | while (!isShutdown) { 83 | dumpToZK(hitQueue, hitCounterMap, hitPathPrefix); 84 | dumpToZK(requireQueue, requireCounterMap, requirePathPrefix); 85 | } 86 | }); 87 | } 88 | 89 | private String processProductName(String productName) { 90 | if (!productName.startsWith("/")) { 91 | productName = "/" + productName; 92 | } 93 | 94 | if (!productName.endsWith("/")) { 95 | productName = productName + "/"; 96 | } 97 | 98 | return productName; 99 | } 100 | 101 | @Override 102 | public void hitIncr(String pattern, int count) { 103 | if (count != 0) 104 | hitQueue.add(Pair.of(pattern, count)); 105 | } 106 | 107 | @Override 108 | public void reqIncr(String pattern, int count) { 109 | if (count != 0) 110 | requireQueue.add(Pair.of(pattern, count)); 111 | } 112 | 113 | @Override 114 | public Map getShooting() { 115 | Map result = new LinkedHashMap<>(); 116 | 117 | AtomicLong totalHit = new AtomicLong(0L); 118 | AtomicLong totalRequire = new AtomicLong(0L); 119 | this.requireCounterMap.forEach((key, requireCounter) -> { 120 | try { 121 | long require = getValue(requireCounter.get()); 122 | long hit = getValue(hitCounterMap.get(key)); 123 | 124 | totalRequire.addAndGet(require); 125 | totalHit.addAndGet(hit); 126 | 127 | result.put(key, ShootingDO.newInstance(hit, require)); 128 | } catch (Exception e) { 129 | LOGGER.error("current zk counter value get error, pattern: {}", key, e); 130 | } 131 | }); 132 | 133 | result.put(summaryName(), ShootingDO.newInstance(totalHit.get(), totalRequire.get())); 134 | 135 | return result; 136 | } 137 | 138 | private long getValue(Object value) throws Exception { 139 | long result = 0L; 140 | if (value != null) { 141 | if (value instanceof DistributedAtomicLong) { 142 | result = getValue(((DistributedAtomicLong) value).get()); 143 | } else if (value instanceof AtomicValue) { 144 | result = (long) ((AtomicValue) value).postValue(); 145 | } else { 146 | result = ((AtomicLong) value).get(); 147 | } 148 | } 149 | 150 | return result; 151 | } 152 | 153 | @Override 154 | public void reset(String pattern) { 155 | hitCounterMap.computeIfPresent(pattern, this::doReset); 156 | requireCounterMap.computeIfPresent(pattern, this::doReset); 157 | } 158 | 159 | @Override 160 | public void resetAll() { 161 | hitCounterMap.forEach(this::doReset); 162 | requireCounterMap.forEach(this::doReset); 163 | } 164 | 165 | private DistributedAtomicLong doReset(String pattern, DistributedAtomicLong counter) { 166 | try { 167 | counter.forceSet(0L); 168 | } catch (Exception e) { 169 | LOGGER.error("reset zk pattern: {} error", pattern, e); 170 | } 171 | 172 | return null; 173 | } 174 | 175 | @PreDestroy 176 | public void tearDown() { 177 | while (hitQueue.size() > 0 || requireQueue.size() > 0) { 178 | LOGGER.warn("shooting queue is not empty: [{}]-[{}], waiting...", hitQueue.size(), requireQueue.size()); 179 | try { 180 | TimeUnit.SECONDS.sleep(1); 181 | } catch (InterruptedException ignored) { 182 | } 183 | } 184 | 185 | isShutdown = true; 186 | } 187 | 188 | private void dumpToZK(BlockingQueue> queue, Map counterMap, String zkPrefix) { 189 | long count = 0; 190 | Pair head; 191 | 192 | // 将queue中所有的 || 前100条数据聚合到一个暂存Map中 193 | Map holdMap = new HashMap<>(); 194 | while ((head = queue.poll()) != null && count <= 100) { 195 | holdMap 196 | .computeIfAbsent(head.getLeft(), (key) -> new AtomicLong(0L)) 197 | .addAndGet(head.getRight()); 198 | ++count; 199 | } 200 | 201 | holdMap.forEach((pattern, atomicCount) -> { 202 | String zkPath = String.format("%s/%s", zkPrefix, pattern); 203 | DistributedAtomicLong counter = counterMap.computeIfAbsent(pattern, (key) -> new DistributedAtomicLong(client, zkPath, new RetryNTimes(10, 10))); 204 | try { 205 | LOGGER.info("zkPath: {} current value : {}", zkPath, counter.add(atomicCount.get()).postValue()); 206 | } catch (Exception e) { 207 | LOGGER.error("update zkPath: {} count offset:{} error", zkPath, atomicCount); 208 | } 209 | }); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/utils/CacheXInfoContainer.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.utils; 2 | 3 | import com.github.cachex.CacheKey; 4 | import com.github.cachex.Cached; 5 | import com.github.cachex.CachedGet; 6 | import com.github.cachex.Invalid; 7 | import com.github.cachex.domain.CacheXAnnoHolder; 8 | import com.github.cachex.domain.CacheXMethodHolder; 9 | import com.github.cachex.domain.Pair; 10 | import com.github.cachex.enums.Expire; 11 | import com.github.cachex.exception.CacheXException; 12 | import com.google.common.base.Strings; 13 | 14 | import java.lang.annotation.Annotation; 15 | import java.lang.reflect.Method; 16 | import java.util.Collection; 17 | import java.util.LinkedHashMap; 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.ConcurrentMap; 21 | 22 | /** 23 | * 定位: 将@Cached、@Invalid、@CachedGet、(@CachedPut未来)以及将@CacheKey整体融合到一起 24 | * 25 | * @author jifang 26 | * @since 16/7/20 上午11:49. 27 | */ 28 | public class CacheXInfoContainer { 29 | 30 | private static final ConcurrentMap> cacheMap = new ConcurrentHashMap<>(); 31 | 32 | public static Pair getCacheXInfo(Method method) { 33 | return cacheMap.computeIfAbsent(method, CacheXInfoContainer::doGetMethodInfo); 34 | } 35 | 36 | private static Pair doGetMethodInfo(Method method) { 37 | CacheXAnnoHolder cacheXAnnoHolder = getAnnoHolder(method); 38 | CacheXMethodHolder cacheXMethodHolder = getMethodHolder(method, cacheXAnnoHolder); 39 | 40 | return Pair.of(cacheXAnnoHolder, cacheXMethodHolder); 41 | } 42 | 43 | /**** 44 | * cache key doGetMethodInfo 45 | ****/ 46 | 47 | private static CacheXAnnoHolder getAnnoHolder(Method method) { 48 | 49 | CacheXAnnoHolder.Builder builder = CacheXAnnoHolder.Builder.newBuilder(method); 50 | 51 | Annotation[][] pAnnotations = method.getParameterAnnotations(); 52 | scanKeys(builder, pAnnotations); 53 | 54 | if (method.isAnnotationPresent(Cached.class)) { 55 | scanCached(builder, method.getAnnotation(Cached.class)); 56 | } else if (method.isAnnotationPresent(CachedGet.class)) { 57 | scanCachedGet(builder, method.getAnnotation(CachedGet.class)); 58 | } else { 59 | scanInvalid(builder, method.getAnnotation(Invalid.class)); 60 | } 61 | 62 | return builder.build(); 63 | } 64 | 65 | private static CacheXAnnoHolder.Builder scanKeys(CacheXAnnoHolder.Builder builder, Annotation[][] pAnnotations) { 66 | int multiIndex = -1; 67 | String id = ""; 68 | Map cacheKeyMap = new LinkedHashMap<>(pAnnotations.length); 69 | 70 | for (int pIndex = 0; pIndex < pAnnotations.length; ++pIndex) { 71 | 72 | Annotation[] annotations = pAnnotations[pIndex]; 73 | for (Annotation annotation : annotations) { 74 | if (annotation instanceof CacheKey) { 75 | CacheKey cacheKey = (CacheKey) annotation; 76 | cacheKeyMap.put(pIndex, cacheKey); 77 | if (isMulti(cacheKey)) { 78 | multiIndex = pIndex; 79 | id = cacheKey.field(); 80 | } 81 | } 82 | } 83 | } 84 | 85 | return builder 86 | .setCacheKeyMap(cacheKeyMap) 87 | .setMultiIndex(multiIndex) 88 | .setId(id); 89 | } 90 | 91 | private static CacheXAnnoHolder.Builder scanCached(CacheXAnnoHolder.Builder builder, Cached cached) { 92 | return builder 93 | .setCache(cached.value()) 94 | .setPrefix(cached.prefix()) 95 | .setExpire(cached.expire()); 96 | } 97 | 98 | private static CacheXAnnoHolder.Builder scanCachedGet(CacheXAnnoHolder.Builder builder, CachedGet cachedGet) { 99 | return builder 100 | .setCache(cachedGet.value()) 101 | .setPrefix(cachedGet.prefix()) 102 | .setExpire(Expire.NO); 103 | } 104 | 105 | private static CacheXAnnoHolder.Builder scanInvalid(CacheXAnnoHolder.Builder builder, Invalid invalid) { 106 | return builder 107 | .setCache(invalid.value()) 108 | .setPrefix(invalid.prefix()) 109 | .setExpire(Expire.NO); 110 | } 111 | 112 | /*** 113 | * cache method doGetMethodInfo 114 | ***/ 115 | 116 | private static CacheXMethodHolder getMethodHolder(Method method, CacheXAnnoHolder cacheXAnnoHolder) { 117 | boolean isCollectionReturn = Collection.class.isAssignableFrom(method.getReturnType()); 118 | boolean isMapReturn = Map.class.isAssignableFrom(method.getReturnType()); 119 | 120 | staticAnalyze(method.getParameterTypes(), 121 | cacheXAnnoHolder, 122 | isCollectionReturn, 123 | isMapReturn); 124 | 125 | return new CacheXMethodHolder(isCollectionReturn); 126 | } 127 | 128 | private static void staticAnalyze(Class[] pTypes, CacheXAnnoHolder cacheXAnnoHolder, 129 | boolean isCollectionReturn, boolean isMapReturn) { 130 | if (isInvalidParam(pTypes, cacheXAnnoHolder)) { 131 | throw new CacheXException("cache need at least one param key"); 132 | } else if (isInvalidMultiCount(cacheXAnnoHolder.getCacheKeyMap())) { 133 | throw new CacheXException("only one multi key"); 134 | } else { 135 | Map cacheKeyMap = cacheXAnnoHolder.getCacheKeyMap(); 136 | for (Map.Entry entry : cacheKeyMap.entrySet()) { 137 | Integer argIndex = entry.getKey(); 138 | CacheKey cacheKey = entry.getValue(); 139 | 140 | if (isMulti(cacheKey) && isInvalidMulti(pTypes[argIndex])) { 141 | throw new CacheXException("multi need a collection instance param"); 142 | } 143 | 144 | if (isMulti(cacheKey) && isInvalidResult(isCollectionReturn, cacheKey.field())) { 145 | throw new CacheXException("multi cache && collection method return need a result field"); 146 | } 147 | 148 | if (isInvalidIdentifier(isMapReturn, isCollectionReturn, cacheKey.field())) { 149 | throw new CacheXException("id method a collection return method"); 150 | } 151 | } 152 | } 153 | } 154 | 155 | private static boolean isMulti(CacheKey cacheKey) { 156 | if (cacheKey == null) { 157 | return false; 158 | } 159 | 160 | String value = cacheKey.value(); 161 | if (Strings.isNullOrEmpty(value)) { 162 | return false; 163 | } 164 | 165 | return value.contains("#i"); 166 | } 167 | 168 | private static boolean isInvalidParam(Class[] pTypes, CacheXAnnoHolder cacheXAnnoHolder) { 169 | Map cacheKeyMap = cacheXAnnoHolder.getCacheKeyMap(); 170 | String prefix = cacheXAnnoHolder.getPrefix(); 171 | 172 | return (pTypes == null 173 | || pTypes.length == 0 174 | || cacheKeyMap.isEmpty()) 175 | && Strings.isNullOrEmpty(prefix); 176 | } 177 | 178 | private static boolean isInvalidMultiCount(Map keyMap) { 179 | int multiCount = 0; 180 | for (CacheKey cacheKey : keyMap.values()) { 181 | if (isMulti(cacheKey)) { 182 | ++multiCount; 183 | if (multiCount > 1) { 184 | break; 185 | } 186 | } 187 | } 188 | 189 | return multiCount > 1; 190 | } 191 | 192 | private static boolean isInvalidIdentifier(boolean isMapReturn, 193 | boolean isCollectionReturn, 194 | String field) { 195 | if (isMapReturn && !Strings.isNullOrEmpty(field)) { 196 | 197 | CacheXLogger.warn("@CacheKey's 'field = \"{}\"' is useless.", field); 198 | 199 | return false; 200 | } 201 | 202 | return !Strings.isNullOrEmpty(field) && !isCollectionReturn; 203 | } 204 | 205 | private static boolean isInvalidResult(boolean isCollectionReturn, String id) { 206 | return isCollectionReturn && Strings.isNullOrEmpty(id); 207 | } 208 | 209 | private static boolean isInvalidMulti(Class paramType) { 210 | return !Collection.class.isAssignableFrom(paramType) 211 | && !paramType.isArray(); 212 | // 永久不能放开 && !Map.class.isAssignableFrom(paramType); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/github/cachex/support/shooting/AbstractDBShootingMXBean.java: -------------------------------------------------------------------------------- 1 | package com.github.cachex.support.shooting; 2 | 3 | import com.github.cachex.ShootingMXBean; 4 | import com.github.cachex.domain.Pair; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.jdbc.core.JdbcOperations; 8 | import org.yaml.snakeyaml.Yaml; 9 | 10 | import javax.annotation.PreDestroy; 11 | import java.io.InputStream; 12 | import java.util.*; 13 | import java.util.concurrent.*; 14 | import java.util.concurrent.atomic.AtomicLong; 15 | import java.util.concurrent.locks.Lock; 16 | import java.util.concurrent.locks.ReentrantLock; 17 | import java.util.function.Supplier; 18 | import java.util.stream.Collectors; 19 | import java.util.stream.Stream; 20 | 21 | /** 22 | * @author jifang.zjf 23 | * @since 2017/6/12 下午12:55. 24 | */ 25 | public abstract class AbstractDBShootingMXBean implements ShootingMXBean { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDBShootingMXBean.class); 28 | 29 | private static final ExecutorService executor = Executors.newSingleThreadExecutor(r -> { 30 | Thread thread = new Thread(r); 31 | thread.setName("cachex:db-shooting-writer"); 32 | thread.setDaemon(true); 33 | return thread; 34 | }); 35 | 36 | private static final Lock lock = new ReentrantLock(); 37 | 38 | private volatile boolean isShutdown = false; 39 | 40 | private BlockingQueue> hitQueue = new LinkedTransferQueue<>(); 41 | 42 | private BlockingQueue> requireQueue = new LinkedTransferQueue<>(); 43 | 44 | private JdbcOperations jdbcOperations; 45 | 46 | private Properties sqls; 47 | 48 | /** 49 | * 1. create JdbcOperations 50 | * 2. init db(like: load sql script, create table, init table...) 51 | * 52 | * @param dbPath :EmbeddedDatabase file temporary storage directory or remove database. 53 | * @param context :other parameters from constructor 54 | * @return initiated JdbOperations object 55 | */ 56 | protected abstract Supplier jdbcOperationsSupplier(String dbPath, Map context); 57 | 58 | /** 59 | * convert DB Map Result to DataDO(Stream) 60 | * 61 | * @param mapResults: {@code List>} result from query DB. 62 | * @return 63 | */ 64 | protected abstract Stream transferResults(List> mapResults); 65 | 66 | protected AbstractDBShootingMXBean(String dbPath, Map context) { 67 | InputStream resource = this.getClass().getClassLoader().getResourceAsStream("sql.yaml"); 68 | this.sqls = new Yaml().loadAs(resource, Properties.class); 69 | 70 | this.jdbcOperations = jdbcOperationsSupplier(dbPath, context).get(); 71 | executor.submit(() -> { 72 | while (!isShutdown) { 73 | dumpToDB(hitQueue, "hit_count"); 74 | dumpToDB(requireQueue, "require_count"); 75 | } 76 | }); 77 | } 78 | 79 | private void dumpToDB(BlockingQueue> queue, String column) { 80 | long times = 0; 81 | Pair head; 82 | 83 | // gather queue's all or before 100 data to a Map 84 | Map holdMap = new HashMap<>(); 85 | while ((head = queue.poll()) != null && times <= 100) { 86 | holdMap 87 | .computeIfAbsent(head.getLeft(), (key) -> new AtomicLong(0L)) 88 | .addAndGet(head.getRight()); 89 | ++times; 90 | } 91 | 92 | // batch write to DB 93 | holdMap.forEach((pattern, count) -> countAddCas(column, pattern, count.get())); 94 | } 95 | 96 | @Override 97 | public void hitIncr(String pattern, int count) { 98 | if (count != 0) 99 | hitQueue.add(Pair.of(pattern, count)); 100 | } 101 | 102 | @Override 103 | public void reqIncr(String pattern, int count) { 104 | if (count != 0) 105 | requireQueue.add(Pair.of(pattern, count)); 106 | } 107 | 108 | @Override 109 | public Map getShooting() { 110 | List dataDOS = queryAll(); 111 | AtomicLong statisticsHit = new AtomicLong(0); 112 | AtomicLong statisticsRequired = new AtomicLong(0); 113 | 114 | // gather pattern's hit rate 115 | Map result = dataDOS.stream().collect(Collectors.toMap( 116 | DataDO::getPattern, 117 | (dataDO) -> { 118 | statisticsHit.addAndGet(dataDO.hitCount); 119 | statisticsRequired.addAndGet(dataDO.requireCount); 120 | return ShootingDO.newInstance(dataDO.hitCount, dataDO.requireCount); 121 | }, 122 | ShootingDO::mergeShootingDO, 123 | LinkedHashMap::new 124 | )); 125 | 126 | // gather application all pattern's hit rate 127 | result.put(summaryName(), ShootingDO.newInstance(statisticsHit.get(), statisticsRequired.get())); 128 | 129 | return result; 130 | } 131 | 132 | @Override 133 | public void reset(String pattern) { 134 | jdbcOperations.update(sqls.getProperty("delete"), pattern); 135 | } 136 | 137 | @Override 138 | public void resetAll() { 139 | jdbcOperations.update(sqls.getProperty("truncate")); 140 | } 141 | 142 | private void countAddCas(String column, String pattern, long count) { 143 | Optional dataOptional = queryObject(pattern); 144 | 145 | // if has pattern record, update it. 146 | if (dataOptional.isPresent()) { 147 | DataDO dataDO = dataOptional.get(); 148 | while (update(column, pattern, getObjectCount(dataDO, column, count), dataDO.version) <= 0) { 149 | dataDO = queryObject(pattern).get(); 150 | } 151 | } else { 152 | lock.lock(); 153 | try { 154 | // double check 155 | dataOptional = queryObject(pattern); 156 | if (dataOptional.isPresent()) { 157 | update(column, pattern, count, dataOptional.get().version); 158 | } else { 159 | insert(column, pattern, count); 160 | } 161 | } finally { 162 | lock.unlock(); 163 | } 164 | } 165 | } 166 | 167 | private Optional queryObject(String pattern) { 168 | String selectSql = sqls.getProperty("select"); 169 | List> mapResults = jdbcOperations.queryForList(selectSql, pattern); 170 | 171 | return transferResults(mapResults).findFirst(); 172 | } 173 | 174 | private List queryAll() { 175 | String selectAllQuery = sqls.getProperty("select_all"); 176 | List> mapResults = jdbcOperations.queryForList(selectAllQuery); 177 | 178 | return transferResults(mapResults).collect(Collectors.toList()); 179 | } 180 | 181 | private int insert(String column, String pattern, long count) { 182 | String insertSql = String.format(sqls.getProperty("insert"), column); 183 | 184 | return jdbcOperations.update(insertSql, pattern, count); 185 | } 186 | 187 | private int update(String column, String pattern, long count, long version) { 188 | String updateSql = String.format(sqls.getProperty("update"), column); 189 | 190 | return jdbcOperations.update(updateSql, count, pattern, version); 191 | } 192 | 193 | private long getObjectCount(DataDO data, String column, long countOffset) { 194 | long lastCount = column.equals("hit_count") ? data.hitCount : data.requireCount; 195 | 196 | return lastCount + countOffset; 197 | } 198 | 199 | @PreDestroy 200 | public void tearDown() { 201 | while (hitQueue.size() > 0 || requireQueue.size() > 0) { 202 | LOGGER.warn("shooting queue is not empty: [{}]-[{}], waiting...", hitQueue.size(), requireQueue.size()); 203 | try { 204 | TimeUnit.SECONDS.sleep(1); 205 | } catch (InterruptedException ignored) { 206 | } 207 | } 208 | 209 | isShutdown = true; 210 | } 211 | 212 | protected static final class DataDO { 213 | 214 | private String pattern; 215 | 216 | private long hitCount; 217 | 218 | private long requireCount; 219 | 220 | private long version; 221 | 222 | public void setPattern(String pattern) { 223 | this.pattern = pattern; 224 | } 225 | 226 | public String getPattern() { 227 | return pattern; 228 | } 229 | 230 | public void setHitCount(long hitCount) { 231 | this.hitCount = hitCount; 232 | } 233 | 234 | public void setRequireCount(long requireCount) { 235 | this.requireCount = requireCount; 236 | } 237 | 238 | public void setVersion(long version) { 239 | this.version = version; 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 4.0.0 8 | 9 | com.github.cachex 10 | cachex 11 | 1.7.3-Hessian-SNAPSHOT 12 | jar 13 | 14 | ${project.groupId}:${project.artifactId} 15 | CacheX: Annotation Cache 16 | https://github.com/feiqing/cachex 17 | 2016 18 | 19 | 20 | https://github.com/feiqing/cachex.git 21 | scm:git:https://github.com/feiqing/cachex 22 | 23 | 24 | 25 | 26 | The Apache License, Version 2.0 27 | http://www.apache.org/licenses/LICENSE-2.0.txt 28 | 29 | 30 | 31 | 32 | 33 | feiqing 34 | feiqing.zjf@gmail.com 35 | 36 | 37 | 38 | 39 | Alibaba Group 40 | http://code.alibabatech.com/ 41 | 42 | 43 | 55 | 56 | 57 | releases 58 | http://mvnrepo.alibaba-inc.com/mvn/releases 59 | 60 | 61 | snapshots 62 | http://mvnrepo.alibaba-inc.com/mvn/snapshots 63 | 64 | 65 | 66 | 67 | 1.16.20 68 | 1.9.0-Hessian-SNAPSHOT 69 | 20.0 70 | 4.2.0 71 | 5.0.0.RELEASE 72 | 1.7.25 73 | 1.8.9 74 | 3.20.0-GA 75 | 3.4 76 | 1.0 77 | 3.2.0 78 | 79 | 1.4.195 80 | 10.11.1.1 81 | 1.18 82 | 2.11.0 83 | 84 | 4.0.0 85 | 4.0.38 86 | 1.2.32 87 | 88 | 2.2.8 89 | 0.9 90 | 0.4 91 | 2.9.0 92 | 2.0.0 93 | 3.3.1 94 | 2.0-beta13 95 | 96 | 4.12 97 | 1.1.3 98 | 99 | true 100 | true 101 | 102 | 103 | 104 | 105 | com.github.jbox 106 | jbox 107 | ${jbox.version} 108 | 109 | 110 | org.projectlombok 111 | lombok 112 | ${lombok.version} 113 | provided 114 | 115 | 116 | com.google.guava 117 | guava 118 | ${guava.version} 119 | 120 | 121 | com.google.inject 122 | guice 123 | ${guice.version} 124 | 125 | 126 | com.google.inject.extensions 127 | guice-multibindings 128 | ${guice.version} 129 | 130 | 131 | 132 | org.springframework 133 | spring-expression 134 | ${spring.version} 135 | 136 | 137 | org.aspectj 138 | aspectjweaver 139 | ${aspectj.version} 140 | 141 | 142 | org.slf4j 143 | slf4j-api 144 | ${slf4j.version} 145 | 146 | 147 | org.apache.commons 148 | commons-lang3 149 | ${commons.lang.version} 150 | 151 | 152 | 153 | 154 | 155 | org.apache.commons 156 | commons-proxy 157 | ${commons.proxy.version} 158 | 159 | 160 | org.javassist 161 | javassist 162 | ${javassist.version} 163 | true 164 | 165 | 166 | cglib 167 | cglib-nodep 168 | ${cglib.version} 169 | true 170 | 171 | 172 | 173 | 174 | com.alibaba 175 | fastjson 176 | ${fastjson.version} 177 | true 178 | 179 | 180 | com.caucho 181 | hessian 182 | ${hession.version} 183 | true 184 | 185 | 186 | com.esotericsoftware 187 | kryo 188 | ${kryo.version} 189 | true 190 | 191 | 192 | 193 | 194 | 195 | org.xerial 196 | sqlite-jdbc 197 | 3.19.3 198 | true 199 | 200 | 201 | mysql 202 | mysql-connector-java 203 | 5.1.36 204 | true 205 | 206 | 207 | com.h2database 208 | h2 209 | ${h2.version} 210 | true 211 | 212 | 213 | org.springframework 214 | spring-jdbc 215 | ${spring.version} 216 | true 217 | 218 | 219 | org.springframework 220 | spring-beans 221 | ${spring.version} 222 | true 223 | 224 | 232 | 233 | org.yaml 234 | snakeyaml 235 | ${yaml.version} 236 | true 237 | 238 | 239 | org.apache.curator 240 | curator-recipes 241 | ${zk.version} 242 | true 243 | 244 | 245 | log4j 246 | log4j 247 | 248 | 249 | com.google.guava 250 | guava 251 | 252 | 253 | org.slf4j 254 | slf4j-api 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | com.taobao.tair 263 | tair-mc-client 264 | ${tair.version} 265 | 266 | 267 | org.springframework 268 | spring 269 | 270 | 271 | org.slf4j 272 | slf4j-log4j12 273 | 274 | 275 | org.yaml 276 | snakeyaml 277 | 278 | 279 | com.google.guava 280 | guava 281 | 282 | 283 | org.slf4j 284 | slf4j-api 285 | 286 | 287 | 288 | 289 | org.iq80.leveldb 290 | leveldb 291 | ${level.db.version} 292 | 293 | 294 | com.google.guava 295 | guava 296 | 297 | 298 | true 299 | 300 | 301 | org.iq80.snappy 302 | snappy 303 | ${snappy.version} 304 | true 305 | 306 | 307 | 308 | 309 | redis.clients 310 | jedis 311 | ${jedis.version} 312 | true 313 | 314 | 315 | 316 | com.googlecode.xmemcached 317 | xmemcached 318 | ${memcached.version} 319 | true 320 | 321 | 322 | org.slf4j 323 | slf4j-api 324 | 325 | 326 | 327 | 328 | 329 | org.ehcache 330 | ehcache 331 | ${ehcache.version} 332 | true 333 | 334 | 335 | org.slf4j 336 | slf4j-api 337 | 338 | 339 | 340 | 341 | 342 | org.mapdb 343 | mapdb 344 | ${mapdb.version} 345 | true 346 | 347 | 348 | 349 | 350 | org.springframework 351 | spring-test 352 | ${spring.version} 353 | test 354 | 355 | 356 | org.springframework 357 | spring-context 358 | ${spring.version} 359 | test 360 | 361 | 362 | org.springframework 363 | spring-core 364 | ${spring.version} 365 | 366 | 367 | ch.qos.logback 368 | logback-classic 369 | ${logback.version} 370 | test 371 | 372 | 373 | org.slf4j 374 | slf4j-api 375 | 376 | 377 | 378 | 379 | junit 380 | junit 381 | ${junit.version} 382 | test 383 | 384 | 385 | 386 | 387 | 388 | 389 | src/main/resources 390 | 391 | **/*.yaml 392 | 393 | 394 | 395 | 396 | 397 | 398 | maven-javadoc-plugin 399 | 400 | 401 | attach-javadoc 402 | 403 | jar 404 | 405 | 406 | 407 | 408 | ${javadoc.skip} 409 | public 410 | UTF-8 411 | UTF-8 412 | UTF-8 413 | 414 | http://docs.oracle.com/javase/6/docs/api 415 | 416 | 417 | 418 | 419 | 420 | maven-gpg-plugin 421 | 422 | ${gpg.skip} 423 | 424 | 425 | 426 | sign-artifacts 427 | verify 428 | 429 | sign 430 | 431 | 432 | 433 | 434 | 435 | org.apache.maven.plugins 436 | maven-compiler-plugin 437 | 3.3 438 | 439 | 440 | UTF-8 441 | 1.8 442 | 1.8 443 | 444 | 445 | 446 | org.apache.maven.plugins 447 | maven-resources-plugin 448 | 3.0.0 449 | 450 | UTF-8 451 | 452 | 453 | 454 | org.apache.maven.plugins 455 | maven-source-plugin 456 | 3.0.0 457 | 458 | 459 | attach-sources 460 | verify 461 | 462 | jar-no-fork 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | --------------------------------------------------------------------------------