为什么需要在SpEL表达式中使用完全限定名

963 阅读2分钟

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