SpEL中,T()用来访问Java类型中的静态属性或静态方法。()需要包含类名的全限定名(包名加上类名)。但是,SpEL内置了java.lang包下的类声明,对于java.lang包中类,我们可以只指定类名来访问该类,例如java.lang.String可以通过T(String)访问。
但是,在并发环境下,仅指定(非完全限定的)类名可能会导致性能问题,我们来看下原因。
SpEL在解析表达式时,使用了如下的代码:
//org.springframework.expression.spel.support.StandardTypeLocator
/**
* Find a (possibly unqualified) type reference - first using the type name as-is,
* then trying any registered prefixes if the type name cannot be found.
* @param typeName the type to locate
* @return the class object for the type
* @throws EvaluationException if the type cannot be found
*/
@Override
public Class<?> findType(String typeName) throws EvaluationException {
// Step1. 通过表达式中字面量来加载类,如果我们使用T(String)来访问,这里的typeName的值为"String"
String nameToLookup = typeName;
try {
return ClassUtils.forName(nameToLookup, this.classLoader);
}
catch (ClassNotFoundException ey) {
// try any registered prefixes before giving up
}
//Step2. 如果Step1加载类失败,则在typeName前加上预置的前缀(这里是java.lang),重新尝试加载
for (String prefix : this.knownPackagePrefixes) {
try {
nameToLookup = prefix + '.' + typeName;
return ClassUtils.forName(nameToLookup, this.classLoader);
}
catch (ClassNotFoundException ex) {
// might be a different prefix
}
}
throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName);
}
public static Class<?> forName(String name, @Nullable ClassLoader classLoader)
throws ClassNotFoundException, LinkageError {
Assert.notNull(name, "Name must not be null");
Class<?> clazz = resolvePrimitiveClassName(name);
if (clazz == null) {
// commonClassCache中包含一些常用的类的实例,但是由于其key是类的全限定名,当我们使用非全限定名时,并不会命中cache
// 最后我们会调用ClassLoader去尝试加载这个类
clazz = commonClassCache.get(name);
}
if (clazz != null) {
return clazz;
}
// ...
try {
// clToUse为一个ClassLoader实例,不为null
return (clToUse != null ? clToUse.loadClass(name) : Class.forName(name));
}
catch (ClassNotFoundException ex) {
int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR);
if (lastDotIndex != -1) {
String innerClassName =
name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1);
try {
return (clToUse != null ? clToUse.loadClass(innerClassName) : Class.forName(innerClassName));
}
catch (ClassNotFoundException ex2) {
// Swallow - let original exception get through
}
}
throw ex;
}
}
// 最终,我们会使用java.lang.ClassLoader来尝试加载"String"类
// 加载前,需要对加载过程加锁,防止重复加载
// jvm是通过完全限定名来定位一个类,当使用非完全限定名时,相当于让jvm去加载一个不存在的类,这个过程是相当耗时的
// 这样就导致其他线程被阻塞在加载类之前
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面的代码和注释解释了为什么在SpEL中,最好使用完全限定名,尤其是在并发环境中。 总结一下原因就是: 对于java.lang包中的类,SpEL使用非全限定名时会先尝试用非全限定名加载类(这时会加锁),失败后再加上java.lang前缀重试。 第一次尝试加的锁会导致其他线程被block