Java Cache details and simple implementation

  • 2020-06-03 06:26:21
  • OfStack

Java Cache details and simple implementation

Summary:

Recently, I am working on the project of spring. I want to make a cache, access the database and update the data regularly

There are two functions to implement

The cache can be refreshed immediately via an http request
The cache can be refreshed periodically through its own configured intervals

Do it through Controller

Since you need to refresh the cache via http, the first idea was to make the cache an Controller

The realization of the Controller

The biggest advantage of Controller is that you can inject many dependencies through the configuration of Spring, such as Service dependencies, database dependencies, and so on.

A large amount of code accessing the database and service layers can be reused

Define an Cache interface as follows:


public interface Cache {
  /**
  * Refresh the cache. If succeed, return true, else return false;
  * 
  * @return
  */
  boolean refresh();

  /**
  * How much time it will refresh the cache.
  * 
  * @return
  */
  long interval();
}

However, there is a problem here. The self-written Controller can easily be injected with THE Http service and connect to the Service layer and database layer, but if CacheController implements the Cache interface, it will find it difficult to call the interval function to find the interval.

Since CacheController is also an Bean, you need to find the bean through Spring to invoke it. If you can't find Bean, you can't call Interval, and you can't use another thread to control the cache refresh. To get this Bean you can put all CacheController into one CacheManagerController


@Controller
public class CacheManagerController {

  @Autowired
  private CacheController cache;

  private static ScheduledExecutorService executor = Executors
     .newScheduledThreadPool(1);

  public CacheManagerController() {
   executor.scheduleAtFixedRate(() -> cache.refresh(), 0, cache.interval(),
      TimeUnit.MILLISECONDS);
  }
}

I considered doing this, but found a problem. At the time of CacheManagerController's initialization, when Bean was constructed, various Cache had not been injected into CacheController, and CacheManagerController could not automatically call the scheduling service without putting methods into the constructor. You need to call it manually. However, the entry point of the program is not determined by which Controller to enter. If the interceptor is written, it is tedious and will be executed every time.

At this point, an CacheService is used to implement this problem


public class CacheService {
  public static final long ONE_MINUTE = 60 * 1000;

  private static ScheduledExecutorService executor = Executors
     .newScheduledThreadPool(1);

  public static void register(Cache cache) {
   executor.scheduleAtFixedRate(() -> cache.refresh(), 0, cache.interval(),
      TimeUnit.MILLISECONDS);
  }
}

@Controller
public class CacheController implements Cache {

  // autowire  All sorts of different service , or the repo Connect to database 
  @Autowired
  private Service service;

  public CacheController() {
   CacheService.register(this);
  }

  // cache interface
  @Override
  public long interval() {
   return 1000;
  }

  @Override
  public boolean refresh() {
   return true;
  }
}

Because of specific CacheController is through reflection structure into Bean run by Spring, so you can directly through a no-parameter constructor to register 1, there is no problem, when Spring when loading CacheController will direct call CacheService registration method, will cache registered to CacheService defined in the thread pool, then immediately refresh method, at the same time also can according to the time interval to refresh automatically.

As for getting the specified Cache, it is easier because Cache is itself an Controller, so you can automatically register with Autowire to use other Controller.

Of course, there's nothing wrong with this right now, but when refresh is called right away, you won't get the Service injected by Autowired. Since Spring is all instantiated and then loaded in series 1, if service is called in the refresh function, it is clear that the program must be reporting null pointer exceptions. This is also the problem of using Controller to do Cache. If you wanted to get all instances of Spring loaded, you would have to modify the constructor to inject the instance into the set of Series 1, just like the previous problem, which is to get Bean. If you could get Bean, you could call the instance method directly, and you wouldn't have that much to do.

conclusion

The features of using Controller are as follows:

Code reuse, defined repo layer, service layer code can continue to be used, do not have to rewrite
Due to the issue of THE Spring declaration period, the refresh operation immediately throws an exception and requires a delayed refresh

Do it via Listener

One advantage of Listener is that it can implement ServletContextListener by writing PreloadListener one by one, so that the code can be initialized in advance when Tomcat loads ES115en.xml.

The realization of the Listener


public class PreloadListener implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent servletContextEvent) {
   CacheFactory.init();
  }

  @Override
  public void contextDestroyed(ServletContextEvent servletContextEvent) {

  }
}

Here is the code for ES123en.xml


// web.xml
  <listener>
    <listener-class>com.sapphire.listener.PreloadListener</listener-class>
  </listener>

Of course, there are advantages and disadvantages to using Listener for preloading and problems with Web's declaration cycle.

When Tomcat loads ES133en.xml, the initialization of Listener occurs before the Spring container starts, thus causing a problem. PreloadListener can be called in the code, is certainly not Autowire to any Bean. This is a huge disadvantage against Controller, and you need to rewrite those Service yourself.

In addition, you need to write a separate Controller to refresh the specified cache.


public class CacheFactory {
  private static ConcurrentHashMap<String, Cache> caches = new ConcurrentHashMap<>();
  private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

  private static void register(Cache cache) {
   caches.put(cache.category(), cache);
  }

  private static void registerAll() {
   register(new StockCache());
  }

  public static void init() {
   registerAll();

   for (Cache cache : caches.values()) {
     executorService.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        cache.refresh();
      }
     }, 0, cache.interval(), TimeUnit.MILLISECONDS);
   }
  }

  public static Cache getCache(String key) {
   if (caches.contains(key)) {
     return caches.get(key);
   }
   return null;
  }
}

// cache Interfaces need to be provided interval and refresh In addition, it needs to be provided 1 a category To distinguish the difference Cache
public interface Cache {
  /**
  * Refresh the cache. If succeed, return true, else return false;
  * 
  * @return
  */
  boolean refresh();

  /**
  * How much time it will refresh the cache.
  * 
  * @return
  */
  long interval();

  /**
  * Cache's category. Each cache has distinct category.
  * 
  * @return
  */
  String category();
}

When CacheFactory is completed in this way, the init method can be called in PreloadListener to initialize all Cache to complete the startup of Cache. As you can see, all CacheFactory methods are static and can be called directly by the Controller layer.

After that, the different init methods need to be written separately and placed in their respective refresh methods. Links to databases, etc., need to be created. Different Cache have to rewrite their own initialization methods, and they also have to write something that reads the file configuration and reads some configuration information from the database. In short, it feels like a hassle.

conclusion

With Listener, more flexibility, you can load the information you need into memory before the container starts, but a lot of business code needs to be rewritten, linked to the database, parsed Property, CacheController refreshed flexibly.

Thank you for reading, I hope to help you, thank you for your support to this site!


Related articles: