1, Background
In the normal development process, we usually choose to use Guava's Cache class as the local Cache.
But sometimes, we don't need expiration control and don't want to introduce Guava package. We also choose to use Map as cache simply and rudely.
However, in some business scenarios, some special processing needs to be done when the attributes of the Map are changed.
For example, when the Map does not change, an immutable List should be generated based on the value of the Map.
2, Method
Trying to use Guava's Cache class, it is found that when the CacheBuilder constructs the Cache, it only provides the RemovalListener to listen for removing or replacing elements, but does not provide the listening for events such as adding elements and emptying map s.
Therefore, drawing on the idea of synchronized list, that is, synchronized list is used as a reference to the list held inside the wrapper, we can transform the Map.
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { private static final long serialVersionUID = -7754090372962971524L; final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } SynchronizedList(List<E> list, Object mutex) { super(list, mutex); this.list = list; } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return list.equals(o);} } public int hashCode() { synchronized (mutex) {return list.hashCode();} } public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } public int indexOf(Object o) { synchronized (mutex) {return list.indexOf(o);} } // Omit others }
Reference codes are as follows:
Construct a ModifyHookMap class, which can pass in the underlying Map and the callback function to be perceived through the Builder.
import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Pair<K, V> { private K key; private V value; }
import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Triple<F, S, T> { private F first; private S second; private T third; }
import org.apache.commons.math3.util.Pair; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.function.Consumer; public class ModifyHookMap<K, V> implements Map<K, V> { private Map<K, V> map; private Consumer<ModifyHookMap<K, V>> onModified; private Consumer<Triple<K, V, V>> onPut; private Consumer<Pair<K, V>> onRemove; private Consumer<Map<? extends K, ? extends V>> onPutAll; private Consumer<Void> onClear; private ModifyHookMap(Builder<K, V> builder) { map = builder.map; onModified = builder.onModified; onPut = builder.onPut; onRemove = builder.onRemove; onPutAll = builder.onPutAll; onClear = builder.onClear; } @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public boolean containsKey(Object key) { return map.containsKey(key); } @Override public boolean containsValue(Object value) { return map.containsValue(value); } @Override public V get(Object key) { return map.get(key); } @Override public V put(K key, V value) { V old = map.put(key, value); if (onPut != null) { onPut.accept(new Triple<>(key, value, old)); } if (onModified != null) { onModified.accept(this); } return old; } @Override public V remove(Object key) { V remove = map.remove(key); if (onRemove != null) { onRemove.accept(new Pair<>((K) key, remove)); } if (onModified != null) { onModified.accept(this); } return remove; } @Override public void putAll(Map<? extends K, ? extends V> m) { map.putAll(m); if (onPutAll != null) { onPutAll.accept(m); } if (onModified != null) { onModified.accept(this); } } @Override public void clear() { map.clear(); if (onClear != null) { onClear.accept(null); } if (onModified != null) { onModified.accept(this); } } @Override public Set<K> keySet() { return map.keySet(); } @Override public Collection<V> values() { return map.values(); } @Override public Set<Entry<K, V>> entrySet() { return map.entrySet(); } public static final class Builder<K, V> { private Map<K, V> map; private Consumer<ModifyHookMap<K, V>> onModified; private Consumer<Triple<K, V, V>> onPut; private Consumer<Pair<K, V>> onRemove; private Consumer<Map<? extends K, ? extends V>> onPutAll; private Consumer<Void> onClear; public Builder() { } public Builder<K, V> map(Map<K, V> val) { map = val; return this; } public Builder<K, V> onModified(Consumer<ModifyHookMap<K, V>> val) { onModified = val; return this; } public Builder<K, V> onPut(Consumer<Triple<K, V, V>> val) { onPut = val; return this; } public Builder<K, V> onRemove(Consumer<Pair<K, V>> val) { onRemove = val; return this; } public Builder<K, V> onPutAll(Consumer<Map<? extends K, ? extends V>> val) { onPutAll = val; return this; } public Builder<K, V> onClear(Consumer<Void> val) { onClear = val; return this; } public ModifyHookMap<K, V> build() { return new ModifyHookMap<K, V>(this); } } }
The test code is as follows (in a hurry, JUnit single test is not used here, and the main method is simply used):
import com.alibaba.fastjson.JSON; import com.google.common.collect.ImmutableList; import org.apache.commons.math3.util.Pair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Demo { private static List<Integer> cache = new ArrayList<Integer>(); private static ModifyHookMap<String, Integer> mapWrapper = new ModifyHookMap .Builder<String, Integer>() .map(new HashMap<>()) .onClear((v) -> { System.out.println("implement clear"); }) .onRemove((Pair<String, Integer> pair) -> { System.out.println("implement remove,k:" + pair.getKey() + ";v" + pair.getValue()); }) .onPut((Triple<String, Integer, Integer> triple) -> { System.out.println("implement put,k:" + triple.getFirst() + ";v new:" + triple.getSecond() + ",v old:" + triple.getThird()); }) .onPutAll((Map<? extends String, ? extends Integer> map) -> { System.out.println("implement putAll:" + JSON.toJSONString(map)); }) .onModified((modifyInvokeMap -> { cache = ImmutableList.copyOf(modifyInvokeMap.values()); System.out.println("Perform modification" + JSON.toJSONString(cache)); })) .build(); public static void main(String[] args) { mapWrapper.put("1", 1); mapWrapper.put("1", 3); Map<String, Integer> otherMap = new HashMap<>(); otherMap.put("2", 2); mapWrapper.putAll(otherMap); mapWrapper.remove("2"); mapWrapper.clear(); } }
Output:
Execute put, k:1;v new1,v old:null Perform modification [1] Execute put, k:1;v new3,v old:1 Perform modification [3] Execute putAll: {2 ': 2} Execute modification [3,2] Execute remove, k:2;v2 Perform modification [3] Execute clear Perform modification []
It should be noted that if the key or value passed in the Consumer is modified, the data of the underlying Map will be affected.
Therefore, it is only recommended to perform read operations in the Consumer, not modify operations.
In addition, map and other required parameters can be verified on the build method.
3, Summary
This paper implements its own Map with callback by learning from synchronized list.
What I want to express is that open source code is not omnipotent. When open source code can not meet, sometimes we need to write specific logic to meet business needs.
In addition, the value of learning lies in the application of learning. We should try to flexibly apply the required knowledge to solve business problems, so as to really give full play to the value of reading source code.