Browse Source

jfinal 3.5

James 7 years ago
parent
commit
48ff936e77
29 changed files with 311 additions and 50 deletions
  1. 3 3
      src/main/java/com/jfinal/core/ActionHandler.java
  2. 1 1
      src/main/java/com/jfinal/core/ActionMapping.java
  3. 4 4
      src/main/java/com/jfinal/core/CPI.java
  4. 9 2
      src/main/java/com/jfinal/core/Controller.java
  5. 21 0
      src/main/java/com/jfinal/core/ControllerFactory.java
  6. 27 1
      src/main/java/com/jfinal/core/FastControllerFactory.java
  7. 1 1
      src/main/java/com/jfinal/core/converter/TypeConverter.java
  8. 2 1
      src/main/java/com/jfinal/kit/HttpKit.java
  9. 92 0
      src/main/java/com/jfinal/kit/SyncWriteMap.java
  10. 1 1
      src/main/java/com/jfinal/plugin/activerecord/DbKit.java
  11. 1 1
      src/main/java/com/jfinal/plugin/activerecord/JavaType.java
  12. 1 1
      src/main/java/com/jfinal/plugin/activerecord/TableMapping.java
  13. 1 1
      src/main/java/com/jfinal/plugin/activerecord/generator/TypeMapping.java
  14. 1 1
      src/main/java/com/jfinal/plugin/ehcache/CacheInterceptor.java
  15. 3 2
      src/main/java/com/jfinal/template/Engine.java
  16. 8 4
      src/main/java/com/jfinal/template/EngineConfig.java
  17. 1 1
      src/main/java/com/jfinal/template/Env.java
  18. 2 1
      src/main/java/com/jfinal/template/expr/ast/FieldKit.java
  19. 11 0
      src/main/java/com/jfinal/template/expr/ast/Method.java
  20. 6 5
      src/main/java/com/jfinal/template/expr/ast/MethodKit.java
  21. 2 1
      src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java
  22. 12 10
      src/main/java/com/jfinal/template/ext/directive/DateDirective.java
  23. 3 2
      src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java
  24. 2 2
      src/main/java/com/jfinal/template/ext/directive/RenderDirective.java
  25. 15 2
      src/main/java/com/jfinal/template/ext/spring/JFinalView.java
  26. 1 1
      src/main/java/com/jfinal/template/stat/Symbol.java
  27. 23 0
      src/main/java/com/jfinal/template/stat/ast/Call.java
  28. 53 0
      src/main/java/com/jfinal/template/stat/ast/NullFunction.java
  29. 4 1
      src/main/java/com/jfinal/validate/Validator.java

+ 3 - 3
src/main/java/com/jfinal/core/ActionHandler.java

@@ -71,7 +71,7 @@ public class ActionHandler extends Handler {
 		try {
 			// Controller controller = action.getControllerClass().newInstance();
 			controller = controllerFactory.getController(action.getControllerClass());
-			controller.init(action, request, response, urlPara[0]);
+			controller._init_(action, request, response, urlPara[0]);
 			
 			if (devMode) {
 				if (ActionReporter.isReportAfterInvocation(request)) {
@@ -140,8 +140,8 @@ public class ActionHandler extends Handler {
 			}
 			renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render();
 		} finally {
-			if (controller != null) {
-				controller.clear();
+			if (controllerFactory.recycleController() && controller != null) {
+				controller._clear_();
 			}
 		}
 	}

+ 1 - 1
src/main/java/com/jfinal/core/ActionMapping.java

@@ -38,7 +38,7 @@ public class ActionMapping {
 	protected static final String SLASH = "/";
 	
 	protected Routes routes;
-	protected Map<String, Action> mapping = new HashMap<String, Action>();
+	protected Map<String, Action> mapping = new HashMap<String, Action>(2048, 0.5F);
 	
 	public ActionMapping(Routes routes) {
 		this.routes = routes;

+ 4 - 4
src/main/java/com/jfinal/core/CPI.java

@@ -33,12 +33,12 @@ import javax.servlet.http.HttpServletResponse;
  */
 public class CPI {
 	
-	public static void init(Controller controller, Action action, HttpServletRequest request, HttpServletResponse response, String urlPara) {
-		controller.init(action, request, response, urlPara);
+	public static void _init_(Controller controller, Action action, HttpServletRequest request, HttpServletResponse response, String urlPara) {
+		controller._init_(action, request, response, urlPara);
 	}
 	
-	public static void clear(Controller controller) {
-		controller.clear();
+	public static void _clear_(Controller controller) {
+		controller._clear_();
 	}
 }
 

+ 9 - 2
src/main/java/com/jfinal/core/Controller.java

@@ -64,7 +64,7 @@ public abstract class Controller {
 	private static final String[] NULL_URL_PARA_ARRAY = new String[0];
 	private static final String URL_PARA_SEPARATOR = Config.getConstants().getUrlParaSeparator();
 	
-	void init(Action action, HttpServletRequest request, HttpServletResponse response, String urlPara) {
+	void _init_(Action action, HttpServletRequest request, HttpServletResponse response, String urlPara) {
 		this.action = action;
 		this.request = request;
 		this.response = response;
@@ -73,7 +73,14 @@ public abstract class Controller {
 		render = null;
 	}
 	
-	void clear() {
+	/**
+	 * 在对 Controller 回收使用场景下,如果继承类中声明了属性,则必须要
+	 * 覆盖此方法,调用父类的 clear() 方法并清掉自身的属性,例如:
+	 * 
+	 * super._clear_();
+	 * this.xxx = null;
+	 */
+	protected void _clear_() {
 		action = null;
 		request = null;
 		response = null;

+ 21 - 0
src/main/java/com/jfinal/core/ControllerFactory.java

@@ -24,6 +24,27 @@ public class ControllerFactory {
 	public Controller getController(Class<? extends Controller> controllerClass) throws InstantiationException, IllegalAccessException {
 		return controllerClass.newInstance();
 	}
+	
+	/**
+	 * 判断是否回收 Controller 对象,如果回收的话就需要 return true,
+	 * 那么 ActionHandler 就会调用 controller._clear_() 用于
+	 * 清除属性,可以回收使用 Controller 对象
+	 * 
+	 * 如果用户自已的 controller 或者 BaseController 中声明了属性,
+	 * 并且希望回收使用 controller 对象,那么就必须覆盖 controller
+	 * 的 _clear_() 方法,大致方法如下:
+	 * 
+	 * protected void _clear_() {
+	 *    super._clear_();	// 清除父类属性中的值
+	 *    xxx = null;		// 清除自身属性中的值
+	 * }
+	 * 
+	 * 回收使用 Controller 除了要注意上述说明中的 _clear_() 用法以外,
+	 * 其实现方式见 FastControllerFactory
+	 */
+	public boolean recycleController() {
+		return false;
+	}
 }
 
 

+ 27 - 1
src/main/java/com/jfinal/core/FastControllerFactory.java

@@ -20,7 +20,22 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * FastControllerFactory
+ * FastControllerFactory 用于回收使用 Controller 对象,提升性能
+ * 
+ * 由于 Controller 会被回收利用,所以使用之前一定要确保 controller
+ * 对象中的属性值没有线程安全问题
+ * 
+ * 警告:如果用户自己的 Controller 或者 BaseController 之中声明了属性,
+ *      并且这些属性不能被多线程共享,则不能直接使用 FastControllerFactory,
+ *      否则会有线程安全问题
+ *      
+ *      jfinal 3.5 版本可以通过覆盖 Controller._clear_() 方法来消除这个限制,
+ *      大至代码如下:
+ *      protected void _clear_() {
+ *          super._clear_();		// 调用父类的清除方法清掉父类中的属性值
+ *          this.xxx = null;		// 清除本类中声明的属性的值
+ *      }
+ *      
  */
 public class FastControllerFactory extends ControllerFactory {
 	
@@ -38,6 +53,17 @@ public class FastControllerFactory extends ControllerFactory {
 		}
 		return ret;
 	}
+	
+	/**
+	 * 返回 true,告知 ActionHandler 该 ControllerFactory 实现类
+	 * 需要回收使用 Controller 对象,则 ActionHandler 会在
+	 * finally 块中调用 Controller._clear_() 方法,确保下一个
+	 * 线程在使用被回收的 controller 时,其中的状态已被清除
+	 */
+	@Override
+	public boolean recycleController() {
+		return true;
+	}
 }
 
 

+ 1 - 1
src/main/java/com/jfinal/core/converter/TypeConverter.java

@@ -62,7 +62,7 @@ import com.jfinal.core.converter.Converters.TimestampConverter;
  */
 public class TypeConverter {
 	
-	private final Map<Class<?>, IConverter<?>> converterMap = new HashMap<Class<?>, IConverter<?>>();
+	private final Map<Class<?>, IConverter<?>> converterMap = new HashMap<Class<?>, IConverter<?>>(64);
 	private static TypeConverter me = new TypeConverter();
 	
 	private TypeConverter() {

+ 2 - 1
src/main/java/com/jfinal/kit/HttpKit.java

@@ -279,11 +279,12 @@ public class HttpKit {
 		} catch (IOException e) {
 			throw new RuntimeException(e);
 		}
+		/* 去掉 close() 否则后续 ActionReporter 中的 getPara() 在部分 tomcat 中会报 IOException : Stream closed
 		finally {
 			if (br != null) {
 				try {br.close();} catch (IOException e) {LogKit.error(e.getMessage(), e);}
 			}
-		}
+		}*/
 	}
 	
 	@Deprecated

+ 92 - 0
src/main/java/com/jfinal/kit/SyncWriteMap.java

@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2011-2019, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * SyncWriteMap 同步写 HashMap
+ * 创建原因是 HashMap扩容时,遇到并发修改可能造成 100% CPU 占用
+ * 
+ * SyncWriteMap 拥有 HashMap 的性能,但不保障并发访问的线程安全
+ * 只用于读多写少且不用保障线程安全的场景
+ * 
+ * 例如 MethodKit 中用于缓存 MethodInfo 的 cache,被写入的数据
+ * 不用保障是单例,读取之后会做 null 值判断
+ * 
+ * ActionMapping 中的 HashMap 是系统启动时在独立线程内初始化的,
+ * 不存在并发写,只存在并发读的情况,所以仍然可以使用 HashMap
+ */
+public class SyncWriteMap<K, V> extends HashMap<K, V> {
+	
+	private static final long serialVersionUID = -7287230891751869148L;
+	
+	public SyncWriteMap() {
+	}
+	
+	public SyncWriteMap(int initialCapacity) {
+		super(initialCapacity);
+	}
+	
+	public SyncWriteMap(int initialCapacity, float loadFactor) {
+		super(initialCapacity, loadFactor);
+	}
+	
+	public SyncWriteMap(Map<? extends K, ? extends V> m) {
+		super(m);
+	}
+	
+	@Override
+	public V put(K key, V value) {
+		synchronized (this) {
+			return super.put(key, value);
+		}
+	}
+	
+	@Override
+	public V putIfAbsent(K key, V value) {
+		synchronized (this) {
+			return super.putIfAbsent(key, value);
+		}
+	}
+	
+	@Override
+	public void putAll(Map<? extends K, ? extends V> m) {
+		synchronized (this) {
+			super.putAll(m);
+		}
+	}
+	
+	@Override
+	public V remove(Object key) {
+		synchronized (this) {
+			return super.remove(key);
+		}
+	}
+	
+	@Override
+	public void clear() {
+		synchronized (this) {
+			super.clear();
+		}
+	}
+}
+
+
+
+

+ 1 - 1
src/main/java/com/jfinal/plugin/activerecord/DbKit.java

@@ -42,7 +42,7 @@ public final class DbKit {
 	 */
 	static Config brokenConfig = Config.createBrokenConfig();
 	
-	private static Map<Class<? extends Model>, Config> modelToConfig = new HashMap<Class<? extends Model>, Config>();
+	private static Map<Class<? extends Model>, Config> modelToConfig = new HashMap<Class<? extends Model>, Config>(512, 0.5F);
 	private static Map<String, Config> configNameToConfig = new HashMap<String, Config>();
 	
 	static final Object[] NULL_PARA_ARRAY = new Object[0];

+ 1 - 1
src/main/java/com/jfinal/plugin/activerecord/JavaType.java

@@ -28,7 +28,7 @@ import java.util.Map;
 public class JavaType {
 	
 	@SuppressWarnings("serial")
-	private Map<String, Class<?>> strToType = new HashMap<String, Class<?>>() {{
+	private Map<String, Class<?>> strToType = new HashMap<String, Class<?>>(32) {{
 		
 		// varchar, char, enum, set, text, tinytext, mediumtext, longtext
 		put("java.lang.String", java.lang.String.class);

+ 1 - 1
src/main/java/com/jfinal/plugin/activerecord/TableMapping.java

@@ -24,7 +24,7 @@ import java.util.Map;
  */
 public class TableMapping {
 	
-	private final Map<Class<? extends Model<?>>, Table> modelToTableMap = new HashMap<Class<? extends Model<?>>, Table>();
+	private final Map<Class<? extends Model<?>>, Table> modelToTableMap = new HashMap<Class<? extends Model<?>>, Table>(512, 0.5F);
 	
 	private static TableMapping me = new TableMapping(); 
 	

+ 1 - 1
src/main/java/com/jfinal/plugin/activerecord/generator/TypeMapping.java

@@ -29,7 +29,7 @@ import java.util.Map;
 public class TypeMapping {
 	
 	@SuppressWarnings("serial")
-	protected Map<String, String> map = new HashMap<String, String>() {{
+	protected Map<String, String> map = new HashMap<String, String>(32) {{
 		// java.util.Data can not be returned
 		// java.sql.Date, java.sql.Time, java.sql.Timestamp all extends java.util.Data so getDate can return the three types data
 		// put("java.util.Date", "java.util.Date");

+ 1 - 1
src/main/java/com/jfinal/plugin/ehcache/CacheInterceptor.java

@@ -37,7 +37,7 @@ import com.jfinal.render.Render;
 public class CacheInterceptor implements Interceptor {
 	
 	private static final String renderKey = "_renderKey";
-	private static ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<String, ReentrantLock>();
+	private static ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<String, ReentrantLock>(512);
 	
 	private ReentrantLock getLock(String key) {
 		ReentrantLock lock = lockMap.get(key);

+ 3 - 2
src/main/java/com/jfinal/template/Engine.java

@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.Map;
 import com.jfinal.kit.HashKit;
 import com.jfinal.kit.StrKit;
+import com.jfinal.kit.SyncWriteMap;
 import com.jfinal.template.expr.ast.MethodKit;
 import com.jfinal.template.source.ClassPathSourceFactory;
 import com.jfinal.template.source.ISource;
@@ -42,7 +43,7 @@ public class Engine {
 	public static final String MAIN_ENGINE_NAME = "main";
 	
 	private static Engine MAIN_ENGINE;
-	private static Map<String, Engine> engineMap = new HashMap<String, Engine>();
+	private static Map<String, Engine> engineMap = new HashMap<String, Engine>(64, 0.5F);
 	
 	// Create main engine
 	static {
@@ -55,7 +56,7 @@ public class Engine {
 	private EngineConfig config = new EngineConfig();
 	private ISourceFactory sourceFactory = config.getSourceFactory();
 	
-	private Map<String, Template> templateCache = new HashMap<String, Template>();
+	private Map<String, Template> templateCache = new SyncWriteMap<String, Template>(2048, 0.5F);
 	
 	/**
 	 * Create engine without management of JFinal 

+ 8 - 4
src/main/java/com/jfinal/template/EngineConfig.java

@@ -49,14 +49,14 @@ public class EngineConfig {
 	
 	WriterBuffer writerBuffer = new WriterBuffer();
 	
-	private Map<String, Define> sharedFunctionMap = new HashMap<String, Define>();
+	private Map<String, Define> sharedFunctionMap = createSharedFunctionMap();		// new HashMap<String, Define>(512, 0.25F);
 	private List<ISource> sharedFunctionSourceList = new ArrayList<ISource>();		// for devMode only
 	
 	Map<String, Object> sharedObjectMap = null;
 	
 	private OutputDirectiveFactory outputDirectiveFactory = OutputDirectiveFactory.me;
 	private ISourceFactory sourceFactory = new FileSourceFactory();
-	private Map<String, Class<? extends Directive>> directiveMap = new HashMap<String, Class<? extends Directive>>();
+	private Map<String, Class<? extends Directive>> directiveMap = new HashMap<String, Class<? extends Directive>>(64, 0.5F);
 	private SharedMethodKit sharedMethodKit = new SharedMethodKit();
 	
 	private boolean devMode = false;
@@ -180,7 +180,7 @@ public class EngineConfig {
 	 * 开发者可直接使用模板注释功能将不需要的 function 直接注释掉
 	 */
 	private synchronized void reloadSharedFunctionSourceList() {
-		Map<String, Define> newMap = new HashMap<String, Define>();
+		Map<String, Define> newMap = createSharedFunctionMap();
 		for (int i = 0, size = sharedFunctionSourceList.size(); i < size; i++) {
 			ISource source = sharedFunctionSourceList.get(i);
 			String fileName = source instanceof FileSource ? ((FileSource)source).getFileName() : null;
@@ -195,9 +195,13 @@ public class EngineConfig {
 		this.sharedFunctionMap = newMap;
 	}
 	
+	private Map<String, Define> createSharedFunctionMap() {
+		return new HashMap<String, Define>(512, 0.25F);
+	}
+	
 	public synchronized void addSharedObject(String name, Object object) {
 		if (sharedObjectMap == null) {
-			sharedObjectMap = new HashMap<String, Object>();
+			sharedObjectMap = new HashMap<String, Object>(64, 0.25F);
 		} else if (sharedObjectMap.containsKey(name)) {
 			throw new IllegalArgumentException("Shared object already exists: " + name);
 		}

+ 1 - 1
src/main/java/com/jfinal/template/Env.java

@@ -35,7 +35,7 @@ import com.jfinal.template.stat.ast.Define;
 public class Env {
 	
 	protected EngineConfig engineConfig;
-	protected Map<String, Define> functionMap = new HashMap<String, Define>();
+	protected Map<String, Define> functionMap = new HashMap<String, Define>(16, 0.5F);
 	
 	// 代替 Template 持有该属性,便于在 #include 指令中调用 Env.addSource()
 	protected List<ISource> sourceList = null;

+ 2 - 1
src/main/java/com/jfinal/template/expr/ast/FieldKit.java

@@ -18,13 +18,14 @@ package com.jfinal.template.expr.ast;
 
 import java.lang.reflect.Field;
 import java.util.HashMap;
+import com.jfinal.kit.SyncWriteMap;
 
 /**
  * FieldKit
  */
 public class FieldKit {
 	
-	private static final HashMap<Long, Object> fieldCache = new HashMap<Long, Object>();
+	private static final HashMap<Long, Object> fieldCache = new SyncWriteMap<Long, Object>(512, 0.5F);
 	
 	public static Field getField(Long key, Class<?> targetClass, String fieldName) {
 		Object field = fieldCache.get(key);

+ 11 - 0
src/main/java/com/jfinal/template/expr/ast/Method.java

@@ -24,6 +24,17 @@ import com.jfinal.template.stat.Scope;
 
 /**
  * Method : expr '.' ID '(' exprList? ')'
+ * 
+ * 每次通过 MethodKit.getMethod(...) 取 MethodInfo 而不是用属性持有其对象
+ * 是为了支持 target 对象的动态类型,MethodInfo 中的 Method 被调用 15 次以后
+ * 会被 JDK 动态生成 GeneratedAccessorXXX 字节码,性能不是问题
+ * 唯一的性能损耗是从 HashMap 中获取 MethodInfo 对象,可以忽略不计
+ * 
+ * 如果在未来通过结合 #dynamic(boolean) 指令来优化,需要在 Ctrl 中引入一个
+ * boolean dynamic = false 变量,而不能在 Env、Scope 引入该变量
+ * 
+ * 还需要引入一个 NullMethodInfo 以及 isNull() 方法,此优化复杂度提高不少,
+ * 暂时不做此优化
  */
 public class Method extends Expr {
 	

+ 6 - 5
src/main/java/com/jfinal/template/expr/ast/MethodKit.java

@@ -23,6 +23,7 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import com.jfinal.kit.ReflectKit;
+import com.jfinal.kit.SyncWriteMap;
 import com.jfinal.template.ext.extensionmethod.ByteExt;
 import com.jfinal.template.ext.extensionmethod.DoubleExt;
 import com.jfinal.template.ext.extensionmethod.FloatExt;
@@ -37,10 +38,10 @@ import com.jfinal.template.ext.extensionmethod.StringExt;
 public class MethodKit {
 	
 	private static final Class<?>[] NULL_ARG_TYPES = new Class<?>[0];
-	private static final Set<String> forbiddenMethods = new HashSet<String>();
-	private static final Set<Class<?>> forbiddenClasses = new HashSet<Class<?>>();
-	private static final Map<Class<?>, Class<?>> primitiveMap = new HashMap<Class<?>, Class<?>>();
-	private static final HashMap<Long, Object> methodCache = new HashMap<Long, Object>();
+	private static final Set<String> forbiddenMethods = new HashSet<String>(64);
+	private static final Set<Class<?>> forbiddenClasses = new HashSet<Class<?>>(64);
+	private static final Map<Class<?>, Class<?>> primitiveMap = new HashMap<Class<?>, Class<?>>(64);
+	private static final Map<Long, Object> methodCache = new SyncWriteMap<Long, Object>(2048, 0.25F);
 	
 	// 初始化在模板中调用 method 时所在的被禁止使用类
 	static {
@@ -307,7 +308,7 @@ public class MethodKit {
 		}
 	}
 	
-	private static final Map<Class<?>, Class<?>> primitiveToBoxedMap = new HashMap<Class<?>, Class<?>>();
+	private static final Map<Class<?>, Class<?>> primitiveToBoxedMap = new HashMap<Class<?>, Class<?>>(64);
 	
 	// 初始化 primitive type 到 boxed type 的映射
 	static {

+ 2 - 1
src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java

@@ -27,6 +27,7 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import com.jfinal.kit.HashKit;
 import com.jfinal.kit.ReflectKit;
+import com.jfinal.kit.SyncWriteMap;
 
 /**
  * SharedMethodKit
@@ -44,7 +45,7 @@ public class SharedMethodKit {
 	}
 	
 	private final List<SharedMethodInfo> sharedMethodList = new ArrayList<SharedMethodInfo>();
-	private final HashMap<Long, SharedMethodInfo> methodCache = new HashMap<Long, SharedMethodInfo>();
+	private final HashMap<Long, SharedMethodInfo> methodCache = new SyncWriteMap<Long, SharedMethodInfo>(512, 0.5F);
 	
 	public SharedMethodInfo getSharedMethodInfo(String methodName, Object[] argValues) {
 		Class<?>[] argTypes = MethodKit.getArgTypes(argValues);

+ 12 - 10
src/main/java/com/jfinal/template/ext/directive/DateDirective.java

@@ -79,23 +79,25 @@ public class DateDirective extends Directive {
 	
 	private void outputWithoutDatePattern(Env env, Scope scope, Writer writer) {
 		Object value = valueExpr.eval(scope);
-		if (value != null) {
+		if (value instanceof Date) {
 			write(writer, (Date)value, env.getEngineConfig().getDatePattern());
+		} else if (value != null) {
+			throw new TemplateException("The first parameter date of #date directive must be Date type", location);
 		}
 	}
 	
 	private void outputWithDatePattern(Env env, Scope scope, Writer writer) {
 		Object value = valueExpr.eval(scope);
-		if (value == null) {
-			return ;
+		if (value instanceof Date) {
+			Object datePattern = this.datePatternExpr.eval(scope);
+			if (datePattern instanceof String) {
+				write(writer, (Date)value, (String)datePattern);
+			} else {
+				throw new TemplateException("The sencond parameter datePattern of #date directive must be String", location);
+			}
+		} else if (value != null) {
+			throw new TemplateException("The first parameter date of #date directive must be Date type", location);
 		}
-		
-		Object datePattern = this.datePatternExpr.eval(scope);
-		if ( !(datePattern instanceof String) ) {
-			throw new TemplateException("The sencond parameter datePattern of #date directive must be String", location);
-		}
-		
-		write(writer, (Date)value, (String)datePattern);
 	}
 	
 	private void write(Writer writer, Date date, String datePattern) {

+ 3 - 2
src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java

@@ -52,11 +52,12 @@ public class EscapeDirective extends Directive {
 			case '>':
 				ret.append("&gt;");
 				break;
-			case '\"':
+			case '"':
 				ret.append("&quot;");
 				break;
 			case '\'':
-				ret.append("&apos;");	// IE 不支持 &apos; 考虑 &#39;
+				// ret.append("&apos;");	// IE 不支持 &apos; 考虑 &#39;
+				ret.append("&#39;");
 				break;
 			case '&':
 				ret.append("&amp;");

+ 2 - 2
src/main/java/com/jfinal/template/ext/directive/RenderDirective.java

@@ -16,8 +16,8 @@
 
 package com.jfinal.template.ext.directive;
 
-import java.util.HashMap;
 import java.util.Map;
+import com.jfinal.kit.SyncWriteMap;
 import com.jfinal.template.Directive;
 import com.jfinal.template.EngineConfig;
 import com.jfinal.template.Env;
@@ -60,7 +60,7 @@ import com.jfinal.template.stat.ast.StatList;
 public class RenderDirective extends Directive {
 	
 	private String parentFileName;
-	private Map<String, StatInfo> statInfoCache = new HashMap<String,StatInfo>();
+	private Map<String, StatInfo> statInfoCache = new SyncWriteMap<String,StatInfo>(16, 0.5F);
 	
 	public void setExprList(ExprList exprList) {
 		int len = exprList.length();

+ 15 - 2
src/main/java/com/jfinal/template/ext/spring/JFinalView.java

@@ -16,6 +16,7 @@
 
 package com.jfinal.template.ext.spring;
 
+import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -56,8 +57,20 @@ public class JFinalView extends AbstractTemplateView {
 			}
 		}
 		
-		OutputStream os = response.getOutputStream();
-		JFinalViewResolver.engine.getTemplate(getUrl()).render(model, os);
+		try {
+			OutputStream os = response.getOutputStream();
+			JFinalViewResolver.engine.getTemplate(getUrl()).render(model, os);
+		} catch (Exception e) {	// 捕获 ByteWriter.close() 抛出的 RuntimeException
+			Throwable cause = e.getCause();
+			if (cause instanceof IOException) {	// ClientAbortException、EofException 直接或间接继承自 IOException
+				String name = cause.getClass().getSimpleName();
+				if ("ClientAbortException".equals(name) || "EofException".equals(name)) {
+					return ;
+				}
+			}
+			
+			throw e;
+		}
 	}
 
 	@SuppressWarnings({"unchecked", "rawtypes", "deprecation"})

+ 1 - 1
src/main/java/com/jfinal/template/stat/Symbol.java

@@ -58,7 +58,7 @@ enum Symbol {
 	 * 扩展指令在得到 # id ( 序列以后才要求得到正确的后续 Token 序列,否则仅仅 return fail()
 	 */
 	@SuppressWarnings("serial")
-	private static final Map<String, Symbol> keywords = new HashMap<String, Symbol>() {{
+	private static final Map<String, Symbol> keywords = new HashMap<String, Symbol>(64) {{
 		put(Symbol.IF.getName(), IF);
 		put(Symbol.ELSEIF.getName(), ELSEIF);
 		put(Symbol.ELSE.getName(), ELSE);

+ 23 - 0
src/main/java/com/jfinal/template/stat/ast/Call.java

@@ -37,12 +37,15 @@ public class Call extends Stat {
 	private ExprList exprList;
 	private boolean callIfDefined;
 	
+	private Define function;
+	
 	public Call(String funcName, ExprList exprList, boolean callIfDefined) {
 		this.funcName = funcName;
 		this.exprList = exprList;
 		this.callIfDefined = callIfDefined;
 	}
 	
+	/*
 	public void exec(Env env, Scope scope, Writer writer) {
 		Define function = env.getFunction(funcName);
 		if (function != null) {
@@ -52,6 +55,26 @@ public class Call extends Stat {
 		} else {
 			throw new TemplateException("Template function not defined: " + funcName, location);
 		}
+	}*/
+	
+	public void exec(Env env, Scope scope, Writer writer) {
+		if (function == null) {
+			Define temp = env.getFunction(funcName);
+			if (temp == null) {
+				temp = NullFunction.me;
+			}
+			function = temp;
+		}
+		
+		if (function instanceof NullFunction) {
+			if (callIfDefined) {
+				return ;
+			} else {
+				throw new TemplateException("Template function not defined: " + funcName, location);
+			}
+		} else {
+			function.call(env, scope, exprList, writer);
+		}
 	}
 }
 

+ 53 - 0
src/main/java/com/jfinal/template/stat/ast/NullFunction.java

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2011-2019, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.util.ArrayList;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.io.Writer;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * NullFunction 辅助模板函数调用在同一个模板文件中只从 Env 中获取一次 function,
+ * 从而提升性能,详情见 Call.exec(...) 中的使用
+ */
+public class NullFunction extends Define {
+	
+	public static final NullFunction me = new NullFunction();
+	
+	private static final StatList NULL_EXPR_LIST = new StatList(new ArrayList<Stat>(0));
+	
+	private NullFunction() {
+		super("NullFunction can not be call", ExprList.NULL_EXPR_LIST, NULL_EXPR_LIST, null);
+	}
+	
+	@Override
+	public void call(Env env, Scope scope, ExprList exprList, Writer writer) {
+		throw new RuntimeException("NullFunction.call(...) 仅用于性能优化,永远不会被调用");
+	}
+	
+	@Override
+	public void exec(Env env, Scope scope, Writer writer) {
+		throw new RuntimeException("NullFunction.exec(...) 仅用于性能优化,永远不会被调用");
+	}
+}
+
+
+
+
+

+ 4 - 1
src/main/java/com/jfinal/validate/Validator.java

@@ -502,10 +502,13 @@ public abstract class Validator implements Interceptor {
 	}
 	
 	private void validateStringValue(String value, int minLen, int maxLen, String errorKey, String errorMessage) {
-		if (StrKit.isBlank(value)) {
+		if (minLen > 0 && StrKit.isBlank(value)) {
 			addError(errorKey, errorMessage);
 			return ;
 		}
+		if (value == null) {		// 支持 minLen <= 0 且 value == null 的情况
+			value = "";
+		}
 		if (value.length() < minLen || value.length() > maxLen) {
 			addError(errorKey, errorMessage);
 		}