001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.components.cache.interceptor;
017
018
019import com.jfinal.aop.Interceptor;
020import com.jfinal.aop.Invocation;
021import com.jfinal.core.Action;
022import com.jfinal.core.CPI;
023import com.jfinal.core.Controller;
024import com.jfinal.plugin.activerecord.Page;
025import com.jfinal.render.Render;
026import com.jfinal.render.RenderManager;
027import io.jboot.components.cache.AopCache;
028import io.jboot.components.cache.annotation.Cacheable;
029import io.jboot.db.model.JbootModel;
030import io.jboot.exception.JbootException;
031import io.jboot.utils.AnnotationUtil;
032import io.jboot.utils.ClassUtil;
033import io.jboot.utils.ModelUtil;
034import io.jboot.utils.StrUtil;
035import io.jboot.web.render.JbootRenderFactory;
036import io.jboot.web.render.JbootReturnValueRender;
037
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import java.lang.reflect.Method;
041import java.util.*;
042
043/**
044 * 缓存操作的拦截器
045 *
046 * @author michael yang
047 */
048public class CacheableInterceptor implements Interceptor {
049
050    private static final String NULL_VALUE = "NULL_VALUE";
051    public static final String IGNORE_CACHED_ATTRS = "__ignore_cached_attrs";
052
053    //是否开启 Controller 的 Action 缓存
054    //可用在 dev 模式下关闭,生产环境开启的场景,方便调试数据
055    private static boolean actionCacheEnable = true;
056    private static String actionCacheRefreshKey;
057    private static String actionCacheRefreshValue = "1";
058
059    public static boolean isActionCacheEnable() {
060        return actionCacheEnable;
061    }
062
063    public static void setActionCacheEnable(boolean actionCacheEnable) {
064        CacheableInterceptor.actionCacheEnable = actionCacheEnable;
065    }
066
067    public static String getActionCacheRefreshKey() {
068        return actionCacheRefreshKey;
069    }
070
071    public static void setActionCacheRefreshKey(String actionCacheRefreshKey) {
072        CacheableInterceptor.actionCacheRefreshKey = actionCacheRefreshKey;
073    }
074
075    public static String getActionCacheRefreshValue() {
076        return actionCacheRefreshValue;
077    }
078
079    public static void setActionCacheRefreshValue(String actionCacheRefreshValue) {
080        if (actionCacheRefreshValue == null || actionCacheRefreshValue.trim().length() == 0){
081            throw new NullPointerException("actionCacheRefresValue can not be null or empty.");
082        }
083        CacheableInterceptor.actionCacheRefreshValue = actionCacheRefreshValue;
084    }
085
086    @Override
087    public void intercept(Invocation inv) {
088
089        Method method = inv.getMethod();
090        Cacheable cacheable = method.getAnnotation(Cacheable.class);
091        if (cacheable == null || (inv.isActionInvocation() && !actionCacheEnable)) {
092            inv.invoke();
093            return;
094        }
095
096        if (inv.isActionInvocation()) {
097            forController(inv, method, cacheable);
098        } else {
099            forService(inv, method, cacheable);
100        }
101    }
102
103
104    private void forController(Invocation inv, Method method, Cacheable cacheable) {
105        String unlessString = AnnotationUtil.get(cacheable.unless());
106        if (Utils.isUnless(unlessString, method, inv.getArgs())) {
107            inv.invoke();
108            return;
109        }
110
111        Class<?> targetClass = inv.getTarget().getClass();
112        String cacheName = AnnotationUtil.get(cacheable.name());
113        Utils.ensureCacheNameNotBlank(method, cacheName);
114        String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cacheable.key()), targetClass, method, inv.getArgs());
115
116        Controller controller = inv.getController();
117
118        //刷新当前页面缓存
119        if (StrUtil.isNotBlank(actionCacheRefreshKey)
120                && actionCacheRefreshValue.equals(inv.getController().getPara(actionCacheRefreshKey))){
121            inv.invoke();
122            cacheActionContent(cacheName, cacheKey, cacheable.liveSeconds(), inv, method);
123            return;
124        }
125
126        ActionCachedContent actionCachedContent = AopCache.get(cacheName, cacheKey);
127        if (actionCachedContent != null) {
128            renderActionCachedContent(controller, actionCachedContent);
129            return;
130        }
131
132        inv.invoke();
133        cacheActionContent(cacheName, cacheKey, cacheable.liveSeconds(), inv, method);
134    }
135
136
137    /**
138     * 对 action 内容进行缓存
139     *
140     * @param cacheName
141     * @param cacheKey
142     * @param liveSeconds
143     * @param inv
144     * @param method
145     */
146    public static void cacheActionContent(String cacheName, String cacheKey, int liveSeconds, Invocation inv, Method method) {
147
148        Render render = getControllerRender(inv, method);
149
150        if (render == null){
151            return;
152        }
153
154        ActionCachedContent cachedContent = new ActionCachedContent(render);
155
156        Controller controller = inv.getController();
157
158        // 忽略的缓存配置
159        Set<String> ignoreCachedAttrs = controller.getAttr(IGNORE_CACHED_ATTRS);
160        if (ignoreCachedAttrs != null) {
161            ignoreCachedAttrs.add(IGNORE_CACHED_ATTRS);
162        }
163
164        HttpServletRequest request = controller.getRequest();
165        for (Enumeration<String> names = request.getAttributeNames(); names.hasMoreElements(); ) {
166            String name = names.nextElement();
167            if (ignoreCachedAttrs == null || !ignoreCachedAttrs.contains(name)) {
168                cachedContent.addAttr(name, request.getAttribute(name));
169            }
170        }
171
172        HttpServletResponse response = controller.getResponse();
173        Collection<String> headerNames = response.getHeaderNames();
174        headerNames.forEach(name -> cachedContent.addHeader(name, response.getHeader(name)));
175
176        AopCache.putDataToCache(cacheName, cacheKey, cachedContent, liveSeconds);
177    }
178
179
180    protected static final RenderManager renderManager = RenderManager.me();
181
182    private static Render getControllerRender(Invocation inv, Method method) {
183        Render render = inv.getController().getRender();
184        if (render == null && void.class != method.getReturnType()
185                && renderManager.getRenderFactory() instanceof JbootRenderFactory) {
186
187            JbootRenderFactory factory = (JbootRenderFactory) renderManager.getRenderFactory();
188            JbootReturnValueRender returnValueRender = factory.getReturnValueRender(inv.getReturnValue());
189
190            //有可能为 null,比如 Forward 的情况
191            render = returnValueRender.getRealRender();
192
193        }else if (render == null) {
194            Action action = CPI.getAction(inv.getController());
195            render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName());
196        }
197
198        return render;
199    }
200
201
202    /**
203     * 渲染缓存的 ActionCachedContent
204     *
205     * @param controller
206     * @param actionCachedContent
207     */
208    private void renderActionCachedContent(Controller controller, ActionCachedContent actionCachedContent) {
209        Map<String, Object> cachedAttrs = actionCachedContent.getAttrs();
210        if (cachedAttrs != null) {
211            HttpServletRequest request = controller.getRequest();
212            Set<String> existAttrNames = getRequestAttrNames(request);
213            cachedAttrs.forEach((cachedAttrName, value) -> {
214                if (!existAttrNames.contains(cachedAttrName)) {
215                    request.setAttribute(cachedAttrName, value);
216                }
217            });
218        }
219
220
221        Map<String, String> headers = actionCachedContent.getHeaders();
222        if (headers != null) {
223            HttpServletResponse response = controller.getResponse();
224            headers.forEach((name, value) -> {
225                String existHeaderValue = response.getHeader(name);
226                if (existHeaderValue != null) {
227                    value = existHeaderValue;
228                }
229                response.setHeader(name, value);
230            });
231        }
232
233        controller.render(actionCachedContent.createRender());
234    }
235
236
237    private Set<String> getRequestAttrNames(HttpServletRequest request) {
238        Set<String> ret = new HashSet<>();
239        for (Enumeration<String> attrNames = request.getAttributeNames(); attrNames.hasMoreElements(); ) {
240            ret.add(attrNames.nextElement());
241        }
242        return ret;
243    }
244
245    /**
246     * Service 层的 Cacheable 使用
247     *
248     * @param inv
249     * @param method
250     * @param cacheable
251     */
252    private void forService(Invocation inv, Method method, Cacheable cacheable) {
253        String unlessString = AnnotationUtil.get(cacheable.unless());
254        if (Utils.isUnless(unlessString, method, inv.getArgs())) {
255            inv.invoke();
256            return;
257        }
258
259        Class<?> targetClass = inv.getTarget().getClass();
260        String cacheName = AnnotationUtil.get(cacheable.name());
261        Utils.ensureCacheNameNotBlank(method, cacheName);
262        String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cacheable.key()), targetClass, method, inv.getArgs());
263
264        Object data = AopCache.get(cacheName, cacheKey);
265        if (data != null) {
266            if (NULL_VALUE.equals(data)) {
267                inv.setReturnValue(null);
268            } else if (cacheable.returnCopyEnable()) {
269                inv.setReturnValue(getCopyObject(inv, data));
270            } else {
271                inv.setReturnValue(data);
272            }
273        } else {
274            inv.invoke();
275            data = inv.getReturnValue();
276            if (data != null) {
277
278                AopCache.putDataToCache(cacheName, cacheKey, data, cacheable.liveSeconds());
279
280                //当启用返回 copy 值的时候,返回的内容应该是一个进行copy之后的值
281                if (cacheable.returnCopyEnable()) {
282                    inv.setReturnValue(getCopyObject(inv, data));
283                }
284
285            } else if (cacheable.nullCacheEnable()) {
286                AopCache.putDataToCache(cacheName, cacheKey, NULL_VALUE, cacheable.liveSeconds());
287            }
288        }
289    }
290
291
292    private <M extends JbootModel> Object getCopyObject(Invocation inv, Object data) {
293        if (data instanceof List) {
294            return ModelUtil.copy((List<? extends JbootModel>) data);
295        } else if (data instanceof Set) {
296            return ModelUtil.copy((Set<? extends JbootModel>) data);
297        } else if (data instanceof Page) {
298            return ModelUtil.copy((Page<? extends JbootModel>) data);
299        } else if (data instanceof JbootModel) {
300            return ModelUtil.copy((JbootModel) data);
301        } else if (data.getClass().isArray()
302                && JbootModel.class.isAssignableFrom(data.getClass().getComponentType())) {
303            return ModelUtil.copy((M[]) data);
304        } else {
305            throw newException(null, inv, data);
306        }
307    }
308
309
310    private JbootException newException(Exception ex, Invocation inv, Object data) {
311        String msg = "Can not copy data for type [" + data.getClass().getName() + "] in method :"
312                + ClassUtil.buildMethodString(inv.getMethod())
313                + " , can not use @Cacheable(returnCopyEnable=true) annotation";
314
315        return ex == null ? new JbootException(msg) : new JbootException(msg, ex);
316    }
317
318
319}