Giter Site home page Giter Site logo

arcus-spring's Introduction

arcus-spring

Arcus as a caching provider for the Spring Cache Abstraction.

arcus-spring provides following functionalities.

  • ArcusClientFactoryBean: Lifecycle management such as creating or deleting an ArcusClient object.
  • ArcusCacheManager: AbstractCacheManager implementation of Spring Cache for managing ArcusCache.
  • ArcusCacheConfiguration: Configuration class representing properties of ArcusCache
  • ArcusCache: Spring Cache implementation for Arcus.
  • StringKeyGenerator, SimpleStringKeyGenerator: KeyGenerator implementation of Spring Cache for generating ArcusStringKey.
  • ArcusStringKey: Arcus subkey class with hash and string key without prefix.
  • Spring 4.3 cache abstract support.

Getting Started

Dependency

The artifact for arcus-spring is in the central Maven repository. To use it, add the following dependency.

Maven (pom.xml)

<dependencies>
    <dependency>
        <groupId>com.jam2in.arcus</groupId>
        <artifactId>arcus-spring</artifactId>
        <version>1.13.6</version>
    </dependency>
</dependencies>

Gradle (build.gradle)

version 7.0 before
dependencies {
    compile 'com.jam2in.arcus:arcus-spring:1.13.6'
}
version 7.0 or later
dependencies {
  implementation 'com.jam2in.arcus:arcus-spring:1.13.6'
}

KeyGenerator

Arcus-spring provides two types of key generator. These two are StringKeyGenerator and SimpleStringKeyGenerator.

  • StringKeyGenerator: it generate the key by combining the parameters and hashcode of the parameters. It's because the key can have invalid characters. This generator replace invalid characters to valid characters. But if the generator do that, even though the keys are different, they can be the same. So the generator adds hashcode to distinguish the keys.
  • SimpleStringKeyGenerator: it generate the key simply by combining the parameters. So this generator can generate invalid keys or duplicate keys.

For example, when the parameters are 'a', 'b', 'c', StringKeyGenerator creates the key 'a,b,c317' and SimpleStringKeyGenerator creates the key 'a,b,c'.

Configuration

Spring Cache configuration is required before using arcus-spring. Create ArcusCacheManager and StringKeyGenerator with the following configuration.

XML

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/cache
                           http://www.springframework.org/schema/cache/spring-cache.xsd
	                       http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util.xsd">

    <cache:annotation-driven
        key-generator="arcusKeyGenerator"
        cache-manager="arcusCacheManager"/>

    <bean id="arcusKeyGenerator"
        class="com.navercorp.arcus.spring.cache.StringKeyGenerator"/>

    <bean id="arcusCacheManager" class="com.navercorp.arcus.spring.cache.ArcusCacheManager">
        <constructor-arg name="adminAddress" value="127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"/>
        <constructor-arg name="serviceCode" value="test"/>
        <constructor-arg name="connectionFactoryBuilder">
            <bean class="net.spy.memcached.ConnectionFactoryBuilder"/>
        </constructor-arg>
        <constructor-arg name="poolSize" value="8"/>
        <!-- default cache configuration (missing cache) -->
        <constructor-arg name="defaultConfiguration" ref="defaultCacheConfig"/>
        <!-- a map of cache configuration (key=cache name, value=cache configuration) -->
        <constructor-arg name="initialCacheConfigs">
            <map>
                <entry key="testCache">
                    <bean parent="defaultCacheConfig">
                        <property name="serviceId" value="TEST-"/>
                        <property name="prefix" value="PRODUCT"/>
                        <property name="expireSeconds" value="60"/>
                    </bean>
                </entry>
                <entry key="devCache">
                    <bean parent="defaultCacheConfig">
                        <property name="serviceId" value="DEV-">
                        <property name="prefix" value="PRODUCT"/>
                        <property name="expireSeconds" value="120"/>
                    </bean>
                </entry>
            </map>
        </constructor-arg>
    </bean>

    <bean id="defaultCacheConfig" class="com.navercorp.arcus.spring.cache.ArcusCacheConfiguration">
        <property name="prefix" value="DEFAULT"/>
        <property name="expireSeconds" value="60"/>
        <property name="timeoutMilliSeconds" value="800"/>
    </bean>

</beans>

Java

@Configuration
@EnableCaching
public class ArcusConfiguration extends CachingConfigurerSupport {

    private static String ADMIN_ADDRESS = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
    private static String SERVICE_CODE = "test";
    private static int POOL_SIZE = 8;

    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new StringKeyGenerator();
    }

    @Bean
    @Override
    public CacheManager cacheManager() {
        return new ArcusCacheManager(
            ADMIN_ADDRESS,
            SERVICE_CODE,
            new ConnectionFactoryBuilder(),
            POOL_SIZE,
            /* default cache configuration (missing cache) */
            defaultCacheConfig(),
            /* a map of cache configuration (key=cache name, value=cache configuration) */
            initialCacheConfig()
        );
    }

    @Bean
    public ArcusCacheConfiguration defaultCacheConfig() {
        ArcusCacheConfiguration defaultCacheConfig = new ArcusCacheConfiguration();
        defaultCacheConfig.setPrefix("DEFAULT");
        defaultCacheConfig.setExpireSeconds(60);
        defaultCacheConfig.setTimeoutMilliSeconds(800);
        return defaultCacheConfig;
    }

    @Bean
    public Map<String, ArcusCacheConfiguration> initialCacheConfig() {
        Map<String, ArcusCacheConfiguration> initialCacheConfig = new HashMap<>();
        initialCacheConfig.put("testCache", testCacheConfig());
        initialCacheConfig.put("devCache", devCacheConfig());
        return initialCacheConfig;
    }

    @Bean
    public ArcusCacheConfiguration testCacheConfig() {
        ArcusCacheConfiguration cacheConfig = new ArcusCacheConfiguration();
        cacheConfig.setServiceId("TEST-");
        cacheConfig.setPrefix("PRODUCT");
        cacheConfig.setExpireSeconds(60);
        cacheConfig.setTimeoutMilliSeconds(800);
        return cacheConfig;
    }

    @Bean
    public ArcusCacheConfiguration devCacheConfig() {
        ArcusCacheConfiguration cacheConfig = new ArcusCacheConfiguration();
        cacheConfig.setServiceId("DEV-");
        cacheConfig.setPrefix("PRODUCT");
        cacheConfig.setExpireSeconds(120);
        cacheConfig.setTimeoutMilliSeconds(800);
        return cacheConfig;
    }

}

Example

Apply the cache using the key(cacheNames) stored in the initialCacheConfig map of ArcusCacheManager you created with XML or Java configuration.

@Service
public class ProductService {

    /*
        using the "testCache" cache with 60 expire seconds and "TEST-PRODUCT" prefix.
    */
    @Cacheable(cacheNames = "testCache", key="#id")
    public Product getProduct_TestCache(int id) {
        return new Product(id);
    }

    /*
        using the "devCache" cache with 120 expire seconds and "DEV-PRODUCT" prefix.
    */
    @Cacheable(cacheNames = "devCache", key="#id")
    public Product getProduct_DevCache(int id) {
        return new Product(id);
    }

    /*
        In ArcusCacheManger, missing cache is loaded with default cache configuration.
        so, the below code uses the default cache with 60 expire seconds and "DEFAULT" prefix.
    */
    @Cacheable(cacheNames = "missingCache", key="#id")
    public Product getProduct_DefaultCache(int id) {
        return new Product(id);
    }

}

Front Cache

You can use the front cache to provide fast responsiveness of cache requests. The front cache takes precedence over ARCUS and performs cache requests. To enable this feature, create an implementation of the ArcusFrontCache interface and set it to the ArcusCacheConfiguration.

Configuration

@Bean
public ArcusCacheConfiguration testCacheConfig() {
    ArcusCacheConfiguration cacheConfig = new ArcusCacheConfiguration();
    cacheConfig.setServiceId("TEST-");
    cacheConfig.setPrefix("PRODUCT");
    cacheConfig.setExpireSeconds(60);
    cacheConfig.setTimeoutMilliSeconds(800);
    /* front cache configuration */
    cacheConfig.setArcusFrontCache(testArcusFrontCache());
    cacheConfig.setFrontExpireSeconds(120);
    cacheConfig.setForceFrontCaching(false);
    /* front cache configuration */
    return cacheConfig;
}

@Bean
public ArcusCacheConfiguration devCacheConfig() {
    ArcusCacheConfiguration cacheConfig = new ArcusCacheConfiguration();
    cacheConfig.setServiceId("DEV-");
    cacheConfig.setPrefix("PRODUCT");
    cacheConfig.setExpireSeconds(120);
    cacheConfig.setTimeoutMilliSeconds(800);
    /* front cache configuration */
    cacheConfig.setArcusFrontCache(devArcusFrontCache());
    cacheConfig.setFrontExpireSeconds(240);
    cacheConfig.setForceFrontCaching(true);
    /* front cache configuration */
    return cacheConfig;
}

@Bean
public ArcusFrontCache testArcusFrontCache() {
    return new DefaultArcusFrontCache("test" /*name*/, 10000 /*maxEntries*/, false /*copyOnRead*/, false /*copyOnWrite*/);
}

@Bean
public ArcusFrontCache devArcusFrontCache() {
    return new DefaultArcusFrontCache("dev" /*name*/, 20000 /*maxEntries*/, false /*copyOnRead*/, false /*copyOnWrite*/);
}

The properties added to the ArcusCache class related to Front Cache are as follows.

  • setArcusFrontCache(ArcusFrontCache arcusFrontCache)
    • Front Cache instance setting. If it is a null value, Front Cache does not work.
  • setFrontExpireSeconds(int frontExpireSeconds)
    • Front Cache TTL(TimeToLive) setting.
  • setForceFrontCaching(int forceFrontCaching)
    • true: Even if the change request of ARCUS fails, the change request is reflected in Front Cache. When a request fails due to an ARCUS failure, the Front Cache function can be worked. But, it is prone to data consistency issues, so we recommend using it only for data that doesn't change frequently.
    • false: If the change request of ARCUS fails, the change request is not reflected in Front Cache.

Front Caching is not always performed. It is performed depending on the attribute of forceFrontCaching property and the result of the ARCUS request.

ArcusCache API ARCUS Result forceFrontCaching=false forceFrontCaching=true
get success O O
get failure X X
put success O O
put failure X O
putIfAbsent success O O
putIfAbsent failure X X
evict success O O
evict failure O O
clear success O O
clear failure X X

DefaultArcusFrontCache

ArcusFrontCache consists of a simple interface for Front Cache. You can implement and use the ArcusFrontCache interface, or you can use the DefaultArcusFrontCache implementation provided by default in the library. Four options are required to use DefaultArcusFrontCache.

  • name
    • Cache name. It must be unique each time an instance is created.
  • maxEntries
    • The maximum number of items that can be stored in the front cache.
  • copyOnRead
    • Whether the Front Cache should copy elements it returns.
  • copyOnWrite
    • Whether the Front Cache should copy elements it gets.

Issues

If you find a bug, please report it via the GitHub issues page.

https://github.com/naver/arcus-spring/issues

License

Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0

arcus-spring's People

Contributors

brido4125 avatar hjyun328 avatar hyeonjae avatar jhpark816 avatar kiheyunkim avatar lynix94 avatar minkikim89 avatar oliviarla avatar sanha avatar uhm0311 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

arcus-spring's Issues

사용자 가이드 문서 작성

1.13.4 버전 기준의 README의 내용을 포함하여 사용자 가이드 문서를 작성한다.

작성할 내용

  • arcus-spring 프로젝트의 개요
  • 의존성 추가 방법 (Maven, Gradle)
  • Bean 생성 방법
    • arcus-spring에서 제공하는 KeyGenerator 구현체 2가지에 대한 설명
  • Cacheable 어노테이션을 사용하는 예시
  • CacheManager를 사용하는 예시
  • FrontCache를 사용하는 예시
    • FrontCache를 사용하지 않을 때와 사용하는 경우의 각 연산에 대한 success, failure 표 작성
    • DefaultArcusFrontCache 사용법
  • 주의 사항
    • spring-devtools 사용 시 ClassLoader에 의한 버그 발생
    • ArcusCache 구현체의 캐시 키 생성 로직에 의한 강제 hash 값 붙임 현상

Spring 5 Webflux의 Cacheable annotation 지원

Spring 5 Webflux 모델에서는 Spring Cache의 annotation을 사용할 수 없다.
WebFlux 모델에서는 실제 Value Object가 아닌 Flux, Mono가 리턴이 되는 구조이기 때문이다.

public Mono<Object> mono() {
   ...
}

Spring 5의 Webflux 모델에 대해 이해하고, WebFlux 구조에서 Cache를 쉽게 적용할 수 있는 방법에 대해 고민하고 arcus-spring에서 이를 지원할 수 있도록 한다.

참고

https://dreamchaser3.tistory.com/17
https://stackoverflow.com/questions/48156424/spring-webflux-and-cacheable-proper-way-of-caching-result-of-mono-flux-type

Setter에 null이 들어가지 않도록 수정

현재 코드 구현 상 아래와 같이 ArcusCache 객체 생성 시 setter로 데이터를 주입해주고 있다.
이 때 사용자가 configuration으로 설정하지 않은 필드는 null값을 가지게 되는데, 이를 그대로 setter에 넣어주어 null 값을 setter에 넣는 형태가 된다.
setter에 null을 입력하는 상황은 혼란을 줄 수 있으므로 setter에 값을 입력하기 전 null인지 검사하고 입력하거나, ArcusCache의 생성자에 ArcusCacheConfiguration 객체를 입력받도록 하는 방안이 괜찮아보인다.

@SuppressWarnings("deprecation")
protected Cache createCache(String name, ArcusCacheConfiguration configuration) {
  ArcusCache cache = new ArcusCache();
  cache.setName(name);
  cache.setServiceId(configuration.getServiceId());
  cache.setPrefix(configuration.getPrefix());
  cache.setArcusClient(client);
  cache.setArcusFrontCache(configuration.getArcusFrontCache());
  cache.setExpireSeconds(configuration.getExpireSeconds());
  cache.setFrontExpireSeconds(configuration.getFrontExpireSeconds());
  cache.setTimeoutMilliSeconds(configuration.getTimeoutMilliSeconds());
  cache.setOperationTranscoder(configuration.getOperationTranscoder());
  cache.setForceFrontCaching(configuration.isForceFrontCaching());
  cache.setWantToGetException(true);

  return cache;
}

createArcusKey()에서 arcusKey 생성 부분 수정.

현재 createArcusKey() 메소드에서 arcusKey 생성 부분이 아래와 같습니다.

  public String createArcusKey(final Object key) {
    String keyString, arcusKey;

    . . .
    arcusKey = serviceId + name + ":" + keyString;
    if (this.prefix != null) {
      arcusKey = serviceId + prefix + ":" + keyString;
    }
    . . .

    return arcusKey;
  }

위의 코드는 불필요하게 GC 부담을 가중시킬 수 있으므로, 아래와 변경하는 것이 좋겠습니다.

    if (this.prefix != null) {
      arcusKey = serviceId + prefix + ":" + keyString;
    } else {
      arcusKey = serviceId + name + ":" + keyString;
    }

KeyGenerator의 새로운 인터페이스 추가 여부 검토

AS-IS

KeyGenerator

현재 KeyGenerator의 유일한 인터페이스는 다음과 같습니다.

Object generate(Object target, Method method, Object... params);

Implementation of KeyGenerator

단, 현재 구현 상 매개변수 target과 method는 사용하지 않습니다.

public class StringKeyGenerator implements KeyGenerator {
  @Override
  public Object generate(Object target, Method method, Object... params) {
    int hash = 0;
    StringBuilder keyBuilder = new StringBuilder();
    for (int i = 0, n = params.length; i < n; i++) {
      if (i > 0) {
        keyBuilder.append(DEFAULT_SEPARTOR);
      }
      if (params[i] != null) {
        keyBuilder.append(params[i]);
        hash ^= ArcusStringKey.light_hash(params[i].toString());
      }
    }

    return new ArcusStringKey(keyBuilder.toString().replace(' ', '_') + hash);
  }
}

public class SimpleStringKeyGenerator implements KeyGenerator {
  @Override
  public Object generate(Object target, Method method, Object... params) {
    StringBuilder keyBuilder = new StringBuilder();
    for (int i = 0, n = params.length; i < n; i++) {
      if (i > 0) {
        keyBuilder.append(DEFAULT_SEPARTOR);
      }
      if (params[i] != null) {
        keyBuilder.append(params[i]);
      }
    }
    return new ArcusStringKey(keyBuilder.toString());
  }
}

Usage

따라서 CacheManager를 직접 주입 받아 사용할 경우, KeyGenerator를 다음과 같이 사용해야 합니다.

@Service
public class ProductService {
    @Autowired
    private CacheManager cacheManager;
    
    @Autowired
    private KeyGenerator keyGenerator;

    /*
        using the "testCache" cache with 60 expire seconds and "TEST-PRODUCT" prefix.
    */
    public Product getProduct_TestCache(int id) {
      Product product = cacheManager.getCache("testCache")
              .get(keyGenerator.generate(null, null, id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }
    
    /*
        using the "devCache" cache with 120 expire seconds and "DEV-PRODUCT" prefix.
    */  
    public Product getProduct_DevCache(int id) {
      Product product = cacheManager.getCache("devCache")
              .get(keyGenerator.generate(null, null, id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }

    /*
        using the "missingCache" cache with 60 expire seconds and "DEFAULT" prefix.
    */
    public Product getProduct_DefaultCache(int id) {
      Product product = cacheManager.getCache("missingCache")
              .get(keyGenerator.generate(null, null, id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }
    
}

Issue

두 매개변수를 null로 주어도 내부적으로 사용하지 않으니 올바르게 동작하지만, 얼핏 코드를 보면 KeyGenerator를 사용하는 인터페이스가 잘못되었단 느낌을 받기 쉽습니다.

TO-BE

따라서 다음과 같은 클래스를 추가하여 매개변수에 null을 넣을 필요 없는 인터페이스를 오버로딩으로 제공하면 좋을 것 같습니다.

New Class

public abstract class ArcusKeyGenerator implements KeyGenerator {
  public Object generate(Object... params) {
    return generate(null, null, params);
  }
}

Implementation of KeyGenerator

public class StringKeyGenerator extends ArcusKeyGenerator { // 상속 관계 변경
  @Override
  public Object generate(Object target, Method method, Object... params) {
    int hash = 0;
    StringBuilder keyBuilder = new StringBuilder();
    for (int i = 0, n = params.length; i < n; i++) {
      if (i > 0) {
        keyBuilder.append(DEFAULT_SEPARTOR);
      }
      if (params[i] != null) {
        keyBuilder.append(params[i]);
        hash ^= ArcusStringKey.light_hash(params[i].toString());
      }
    }

    return new ArcusStringKey(keyBuilder.toString().replace(' ', '_') + hash);
  }
}

public class SimpleStringKeyGenerator extends ArcusKeyGenerator { // 상속 관계 변경
  @Override
  public Object generate(Object target, Method method, Object... params) {
    StringBuilder keyBuilder = new StringBuilder();
    for (int i = 0, n = params.length; i < n; i++) {
      if (i > 0) {
        keyBuilder.append(DEFAULT_SEPARTOR);
      }
      if (params[i] != null) {
        keyBuilder.append(params[i]);
      }
    }
    return new ArcusStringKey(keyBuilder.toString());
  }
}

Usage

@Service
public class ProductService {
    @Autowired
    private CacheManager cacheManager;
    
    @Autowired
    private ArcusKeyGenerator keyGenerator;

    /*
        using the "testCache" cache with 60 expire seconds and "TEST-PRODUCT" prefix.
    */
    public Product getProduct_TestCache(int id) {
      Product product = cacheManager.getCache("testCache")
              .get(keyGenerator.generate(id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }
    
    /*
        using the "devCache" cache with 120 expire seconds and "DEV-PRODUCT" prefix.
    */  
    public Product getProduct_DevCache(int id) {
      Product product = cacheManager.getCache("devCache")
              .get(keyGenerator.generate(id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }

    /*
        using the "missingCache" cache with 60 expire seconds and "DEFAULT" prefix.
    */
    public Product getProduct_DefaultCache(int id) {
      Product product = cacheManager.getCache("missingCache")
              .get(keyGenerator.generate(id), Product.class);
      
      if (product == null) {
        return new Product(id);
      }
      return product;
    }
    
}

다음 버전 release 일정 문의

안녕하세요.
사내에서 스프링 latest 버전으로의 bump up을 준비하고 있는 프로젝트가 있습니다.
#63 PR이 반영된 다음 release 일정이 언제인지 알 수 있을까요?
가능하시다면 minor fix 버전으로라도 패치 해 주신다면 감사하겠습니다!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.