View Javadoc
1   /*
2    * Copyright 2004-2025 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.ibatis.sqlmap.engine.cache;
17  
18  import com.ibatis.common.logging.Log;
19  import com.ibatis.common.logging.LogFactory;
20  import com.ibatis.sqlmap.engine.mapping.statement.ExecuteListener;
21  import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement;
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.Properties;
31  import java.util.Set;
32  
33  /**
34   * Wrapper for Caches.
35   */
36  public class CacheModel implements ExecuteListener {
37  
38    /** The Constant log. */
39    private static final Log log = LogFactory.getLog(CacheModel.class);
40  
41    /** The Constant MAX_OBJECT_LOG_SIZE. */
42    private static final int MAX_OBJECT_LOG_SIZE = 200;
43  
44    /**
45     * This is used to represent null objects that are returned from the cache so that they can be cached, too.
46     */
47    public static final Object NULL_OBJECT = "SERIALIZABLE_NULL_OBJECT";
48  
49    /** The requests. */
50    private int requests = 0;
51  
52    /** The hits. */
53    private int hits = 0;
54  
55    /** Constant to turn off periodic cache flushes. */
56    private static final long NO_FLUSH_INTERVAL = -99999;
57  
58    /** The id. */
59    private String id;
60  
61    /** The read only. */
62    private boolean readOnly;
63  
64    /** The serialize. */
65    private boolean serialize;
66  
67    /** The last flush. */
68    private long lastFlush;
69  
70    /** The flush interval. */
71    private long flushInterval;
72  
73    /** The flush interval seconds. */
74    private long flushIntervalSeconds;
75  
76    /** The flush trigger statements. */
77    private Set flushTriggerStatements;
78  
79    /** The controller. */
80    private CacheController controller;
81  
82    /** The resource. */
83    private String resource;
84  
85    /**
86     * Default constructor.
87     */
88    public CacheModel() {
89      this.flushInterval = NO_FLUSH_INTERVAL;
90      this.flushIntervalSeconds = NO_FLUSH_INTERVAL;
91      this.lastFlush = System.currentTimeMillis();
92      this.flushTriggerStatements = new HashSet<>();
93    }
94  
95    /**
96     * Getter for the cache model's id.
97     *
98     * @return the id
99     */
100   public String getId() {
101     return id;
102   }
103 
104   /**
105    * Setter for the cache model's id.
106    *
107    * @param id
108    *          - the new id
109    */
110   public void setId(String id) {
111     this.id = id;
112   }
113 
114   /**
115    * Getter for read-only property.
116    *
117    * @return true if a read-only model
118    */
119   public boolean isReadOnly() {
120     return readOnly;
121   }
122 
123   /**
124    * Setter for read-only property.
125    *
126    * @param readOnly
127    *          - the new setting
128    */
129   public void setReadOnly(boolean readOnly) {
130     this.readOnly = readOnly;
131   }
132 
133   /**
134    * Getter to tell if the cache serializes.
135    *
136    * @return true if the cache model serializes objects
137    */
138   public boolean isSerialize() {
139     return serialize;
140   }
141 
142   /**
143    * Setter to tell the cache to serialize objects.
144    *
145    * @param serialize
146    *          - if the cache model is to serialize objects
147    */
148   public void setSerialize(boolean serialize) {
149     this.serialize = serialize;
150   }
151 
152   /**
153    * Getter for resource property.
154    *
155    * @return the value of the resource property
156    */
157   public String getResource() {
158     return resource;
159   }
160 
161   /**
162    * Setter for resource property.
163    *
164    * @param resource
165    *          - the new value
166    */
167   public void setResource(String resource) {
168     this.resource = resource;
169   }
170 
171   /**
172    * Sets up the controller for the cache model.
173    *
174    * @param controller
175    *          the new cache controller
176    *
177    * @throws ClassNotFoundException
178    *           - if the class cannot be found
179    * @throws InstantiationException
180    *           - if the class cannot be instantiated
181    * @throws IllegalAccessException
182    *           - if the classes constructor is not accessible
183    */
184   public void setCacheController(CacheController controller)
185       throws ClassNotFoundException, InstantiationException, IllegalAccessException {
186     this.controller = controller;
187   }
188 
189   /**
190    * Getter for flushInterval property.
191    *
192    * @return The flushInterval (in milliseconds)
193    */
194   public long getFlushInterval() {
195     return flushInterval;
196   }
197 
198   /**
199    * Getter for flushInterval property.
200    *
201    * @return The flushInterval (in milliseconds)
202    */
203   public long getFlushIntervalSeconds() {
204     return flushIntervalSeconds;
205   }
206 
207   /**
208    * Setter for flushInterval property.
209    *
210    * @param flushInterval
211    *          The new flushInterval (in milliseconds)
212    */
213   public void setFlushInterval(long flushInterval) {
214     this.flushInterval = flushInterval;
215     this.flushIntervalSeconds = flushInterval / 1000;
216   }
217 
218   /**
219    * Adds a flushTriggerStatment. When a flushTriggerStatment is executed, the cache is flushed (cleared).
220    *
221    * @param statementName
222    *          The statement to add.
223    */
224   public void addFlushTriggerStatement(String statementName) {
225     flushTriggerStatements.add(statementName);
226   }
227 
228   /**
229    * Gets an Iterator containing all flushTriggerStatment objects for this cache.
230    *
231    * @return The Iterator
232    */
233   public Iterator getFlushTriggerStatementNames() {
234     return flushTriggerStatements.iterator();
235   }
236 
237   /**
238    * ExecuteListener event. This will be called by a MappedStatement for which this cache is registered as a
239    * ExecuteListener. It will be called each time an executeXXXXXX method is called. In the case of the Cache class, it
240    * is registered in order to flush the cache whenever a certain statement is executed. (i.e. the flushOnExecute cache
241    * policy)
242    *
243    * @param statement
244    *          The statement to execute
245    */
246   @Override
247   public void onExecuteStatement(MappedStatement statement) {
248     flush();
249   }
250 
251   /**
252    * Returns statistical information about the cache.
253    *
254    * @return the number of cache hits divided by the total requests
255    */
256   public double getHitRatio() {
257     return (double) hits / (double) requests;
258   }
259 
260   /**
261    * Configures the cache.
262    *
263    * @param props
264    *          the props
265    */
266   public void configure(Properties props) {
267     controller.setProperties(props);
268   }
269 
270   /**
271    * Clears the cache.
272    */
273   public void flush() {
274     synchronized (this) {
275       controller.flush(this);
276       lastFlush = System.currentTimeMillis();
277       if (log.isDebugEnabled()) {
278         log("flushed", false, null);
279       }
280     }
281   }
282 
283   /**
284    * Get an object out of the cache. A side effect of this method is that is may clear the cache if it has not been
285    * cleared in the flushInterval.
286    *
287    * @param key
288    *          The key of the object to be returned
289    *
290    * @return The cached object (or null)
291    */
292   public Object getObject(CacheKey key) {
293     Object value = null;
294     synchronized (this) {
295       if (flushInterval != NO_FLUSH_INTERVAL && System.currentTimeMillis() - lastFlush > flushInterval) {
296         flush();
297       }
298 
299       value = controller.getObject(this, key);
300       if (serialize && !readOnly && value != NULL_OBJECT && value != null) {
301         try {
302           ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
303           ObjectInputStream ois = new ObjectInputStream(bis);
304           value = ois.readObject();
305           ois.close();
306         } catch (Exception e) {
307           throw new RuntimeException("Error caching serializable object.  Be sure you're not attempting to use "
308               + "a serialized cache for an object that may be taking advantage of lazy loading.  Cause: " + e, e);
309         }
310       }
311       requests++;
312       if (value != null) {
313         hits++;
314       }
315       if (log.isDebugEnabled()) {
316         if (value != null) {
317           log("retrieved object", true, value);
318         } else {
319           log("cache miss", false, null);
320         }
321       }
322     }
323     return value;
324   }
325 
326   /**
327    * Add an object to the cache.
328    *
329    * @param key
330    *          The key of the object to be cached
331    * @param value
332    *          The object to be cached
333    */
334   public void putObject(CacheKey key, Object value) {
335     if (null == value) {
336       value = NULL_OBJECT;
337     }
338     synchronized (this) {
339       if (serialize && !readOnly && value != NULL_OBJECT) {
340         try {
341           ByteArrayOutputStream bos = new ByteArrayOutputStream();
342           ObjectOutputStream oos = new ObjectOutputStream(bos);
343           oos.writeObject(value);
344           oos.flush();
345           oos.close();
346           value = bos.toByteArray();
347         } catch (IOException e) {
348           throw new RuntimeException("Error caching serializable object.  Cause: " + e, e);
349         }
350       }
351       controller.putObject(this, key, value);
352       if (log.isDebugEnabled()) {
353         log("stored object", true, value);
354       }
355     }
356   }
357 
358   /**
359    * Get the maximum size of an object in the log output.
360    *
361    * @return Maximum size of a logged object in the output
362    */
363   protected int getMaxObjectLogSize() {
364     return MAX_OBJECT_LOG_SIZE;
365   }
366 
367   /**
368    * Log a cache action. Since this method is pretty heavy weight, it's best to enclose it with a log.isDebugEnabled()
369    * when called.
370    *
371    * @param action
372    *          String to output
373    * @param addValue
374    *          Add the value being cached to the log
375    * @param cacheValue
376    *          The value being logged
377    */
378   protected void log(String action, boolean addValue, Object cacheValue) {
379     StringBuilder output = new StringBuilder("Cache '");
380     output.append(getId());
381     output.append("': ");
382     output.append(action);
383     if (addValue) {
384       String cacheObjectStr = cacheValue == null ? "null" : cacheValue.toString();
385       output.append(" '");
386       if (cacheObjectStr.length() < getMaxObjectLogSize()) {
387         output.append(cacheObjectStr);
388       } else {
389         output.append(cacheObjectStr.substring(1, getMaxObjectLogSize()));
390         output.append("...");
391       }
392       output.append("'");
393     }
394     log.debug(output.toString());
395   }
396 
397   /**
398    * Sets the controller properties.
399    *
400    * @param cacheProps
401    *          the new controller properties
402    */
403   public void setControllerProperties(Properties cacheProps) {
404     controller.setProperties(cacheProps);
405   }
406 }