前提知识:java class 类加载过程,为什么要双亲委派机制(上)
如果相同的class文件要如何加载,违背双亲委派机制?
相同包名的两个类在同一个加载器如何加载?按照优先级原则只能加载自己项目的,第三包加载不到
情况如下: 有一个com.alibaba.druid.filter.config.CongfigTools同包名两个类,怎么同时加载。只能分开两种不同的加载器。
//继承ClassLoader的自定义加载器的父类加载器是AppClassLoader,记得修改
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
//自定义加载器
public class DefineClassLoader extends ClassLoader {
//.class文件详细地址,相当于自己的加载器也有jar管理地址
private String dst;
public void setDst(String dst) {
this.dst = dst;
}
public DefineClassLoader(String dst) {
//要设置父加载器是ExtClassLoader,不然默认的父加载器是appClassLoader,由于双亲委派机制待会还是失效的,
//因为AppClassLoader有那个包
super(Launcher.getLauncher().getClassLoader().getParent());
this.dst = dst;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//获取class文件的字节流
byte[] bytes = getClassData(dst);
if (bytes != null) {
//name是类名字 com.alibaba.druid.filter.config.CongfigTools
clazz = defineClass(name, bytes, 0, bytes.length);
}
return clazz;
}
public byte[] getClassData(String dst) {
File file = new File(dst);
if (file.exists()) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int size = 0;
while ((size = in.read(bytes)) != -1) {
out.write(bytes, 0, size);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
return null;
}
}
自定义类加载器其他同包名同类名的类
public class ConfigTools {
private Integer a = Integer.parseInt("1");
public Integer getA() {
return a;
}
public void setA(Integer a) {
this.a = a;
}
public static void main(String[] args) {
System.out.println("aaaa");
}
}
public static void main(String[] args) throws Exception{
ConfigTools filter = new ConfigTools();
System.out.println(filter.getA());
DefineClassLoader defineClassLoader = new DefineClassLoader("/Users/hilbert/.m2/repository/com/alibaba/druid/1.1.22/druid-1.1.22/com/alibaba/druid/filter/config/ConfigTools.class");
Class<?> aClass = defineClassLoader.loadClass("com.alibaba.druid.filter.config.ConfigTools");
ConfigTools instance = (ConfigTools)aClass.newInstance();
System.out.println(JSONObject.toJSONString(instance));
}
运行结果:转化类型失败,说明成功
遇到的坑:
- 重写loadClass 方法和findClass方法
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] bytes = getClassData(dst);
if (bytes != null) {
clazz = defineClass(name, bytes, 0, bytes.length);
}
return clazz;
}
重写loadClass 破坏了双亲委派机制,导致了自定义的类加载器去加载java.lang.object,但被Java安全机制禁止了所以会报错。defineClass调用preDefineClass,preDefineClass 会检查包名,如果以java开头,就会抛出异常,因为让用户自定义的类加载器来加载Java自带的类库会引起混乱。
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " + name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
热部署的实现(拓展知识点)
热部署就是在不重启应用的情况下,当类的定义即字节码文件修改后,能够替换该Class创建的对象。 一般情况下,类的加载都是由系统自带的类加载器完成,且对于同一个全限定名的java类,只能被加载一次,而且无法被卸载。
- 一个监听器:监听class文件或者jar 包的变化
- 利用自定义的 ClassLoader 替换系统的加载器,创建一个新的 ClassLoader
- ClassLoader加载Class文件,得到的 Class 对象就是新的(因为不是同一个类加载器),再用该 Class
对象创建一个实例,从而实现动态更新。
如:修改 JSP 文件即生效,就是利用自定义的 ClassLoader 实现的。
Tomcat 违背双亲委派机制?
双亲委派机制的核心是什么:ClassLoader 的 loadClass 方法是加载Class 类的核心
破坏双亲委派机制:重写ClassLoader的loadClass 方法(就是不按照那个链路顺序,不往上找就是了)
Tomcat 的类加载器分两部分:
- 遵循双亲委派机制的类加载器:CatalinaLoader SharedLoader CommonLoader
- 违背双亲委派机制的类加载器:WebappClassLoader JasperLoader
Common 类加载器: 负责加载 ${catalina.home}/lib, ${catalina.base}/lib目录的类库,这儿存放的类库可被 tomcat以及所有的应用使用。(Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享)
Catalina 类加载器:负责加载 /server 目录的类库,只能被 tomcat使用(为了隔绝Web应用程序与Tomcat本身的类,用来装载Tomcat本身的依赖)
Shared 类加载器: 负载加载 /shared 目录的类库,可被所有的 web 应用使用,但 tomcat 不可使用。
这三个都是URLClassLoader,CommonClassLoader的父类是AppClassLoader 遵循双亲委派机制的,没有重写loadClass方法
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
commonLoader = this.getClass().getClassLoader();
}
//CommonClassLoader 作为catalinaLoader 和 sharedLoader的父类加载器
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//从这个properties 决定自己是加载哪个范围的jar
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<Repository>();
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
CatalinaProperties.loadProperties() 决定了CommonClassLoader , SharedClassLoader ,CatalinaClassLoader 加载的jar 范围
Tomcat的catalina.properties文件位于%CATALINA_HOME%/conf/目录下面,该文件主要配置tomcat的安全设置、类加载设置、不需要扫描的类设置、字符缓存设置四大块。
loadPropertiese(){
....
if (is == null) {
try {
File home = new File(getCatalinaBase());
File conf = new File(home, "conf");
File propsFile = new File(conf, "catalina.properties");
is = new FileInputStream(propsFile);
} catch (Throwable t) {
handleThrowable(t);
}
}
.....
}
//catalina.properties文件如下
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=
shared.loader=
server.loader 和 shared.loader
在common.loader 加载完后,tomcat启动程序会检查catalina.properties文件中配置的server.loader和shared.loader是否设置。
如果设置,读取 tomcat下对应的server和shared这两个目录的类库。server和shared是对应tomcat目录下的两个目录
在Tomcat7中默认这两个目录是没有的。设置方法如下:
server.loader= c a t a l i n a . b a s e / s e r v e r / c l a s s e s , {catalina.base}/server/classes, catalina.base/server/classes,{catalina.base}/server/lib/*.jar
设置CatalinaLoader 和 SharedLoader
public void init() throws Exception {
//设置项目文件路径
setCatalinaHome();
setCatalinaBase();
initClassLoaders();
//加载tomcat本身的类(隔离Web应用跟Tomcat 本身的类)
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
//给Catalina 设置父类类加载器为SharedLoader
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
//Catalina 是Server 的一个属性。 Server 下面的Service 都是 用这个父类加载器,找不到就用SystemClassLoader:AppClassLoader
/**
* @return the parent class loader for this component. If not set, return
* {@link #getCatalina()} {@link Catalina#getParentClassLoader()}. If
* catalina has not been set, return the system class loader.
*/
public ClassLoader getParentClassLoader() {
if (parentClassLoader != null)
return parentClassLoader;
if (catalina != null) {
return catalina.getParentClassLoader();
}
return ClassLoader.getSystemClassLoader();
}
SharedLoader 如何做到Web应用程序共享的?
因为他是WebAppClassLoader的父类 。WebAppClassLoader 是在context 级别的。context以上的host Service Server 用的父类加载器都是,因为这些都是可以不同context 共享的。
Context 是Server.xml 最小配置单位了,所以Context 是 最细的web 应用
###Server.xml 配置文件
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
//监听的端口
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
//最小单位是context localhost:8080/server/aaaaa.jsp
<Context path="" docBase="client" reloadable="true" crossContext="true" />
<Context path="/server" docBase="server" reloadable="true" crossContext="true" />
</Host>
<Host name="www.a.com" appBase="webapps2" unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
<!--开始第二个Service配置-->
<Service name="bbs">
<Connector port="8090" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="bbs" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true">
<Context debug="0" docBase="/srv/www/bbs" path="" privileged="true" reloadable="true"/>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="bbs_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
所以context是相对独立的,每个context都有自己独立的包目录。会存在相同的包 com.xxx.a ,但是不同的实现,所以需要各自加载各自的,隔离开来。所以需要一个新的类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找。这样就做到Web应用层级的隔离。
Context 的标准实现 StandardContext 类的 startInternal 方法 会开启一个web应用时会创建类加载器
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());//添加父类加载器
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
//前面已经说了getParentClassLoader 得到的是SharedClassLoader
//WebappLoader 也有一个 startInternal 方法 所以webAppLoader 的classLoader 是WebappClassLoader
classLoader = createClassLoader();
private String loaderClass = "org.apache.catalina.loader.WebappClassLoader";
private WebappClassLoaderBase createClassLoader()
throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = container.getParentClassLoader();
} else {
container.setParentClassLoader(parentClassLoader);
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
同时会启动类加载器
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
在WebappClassLoader的父类WebappClassLoaderBase中实现了start方法
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
//加载web应用的所有class文件
WebResource classes = resources.getResource("/WEB-INF/classes");
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
//加载web应用lib目录下在jar文件
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
在WebappClassLoaderBase中重写了ClassLoader的loadClass方法。通过设置web应用可以遵循类加载的双亲委派机制或者不遵循双亲委派机制了。
- 从ClassLoader 的本地缓存中加载类
- 调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类
- 通过系统的类加载器加载此类,这里防止应用写的类覆盖了J2SE的类(java 核心类,加载的是AppClassLoader那种。不是SharedLoader那种)
public WebappClassLoaderBase() {
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
}
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
- 是否交给父类去加载。filter方法中根据包名来判断是否需要进行委托加载,默认情况下会返回false.因此delegatedLoad为false
- 因为delegatedLoad为false,那么此时不会委托父加载器SharedLoader去加载,这里其实是没有遵循parent-first的加载机制
- 自身加载
- 交给父类去加载
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//锁住正在加载的类
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
checkStateForClassLoading(name);
//从当前ClassLoader的本地缓存中加载类,如果找到则返回
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// 本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//通过系统的类加载器加载此类,这里防止应用写的类覆盖了J2SE的类,这句代码非常关键,如果不写的话,
//就会造成你自己写的类有可能会把J2SE的类给替换调,另外假如你写了一个javax.servlet.Servlet类,放在当前应用的WEB-INF/class中,
//如果没有此句代码的保证,那么你自己写的类就会替换到Tomcat容器Lib中包含的类。
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
} catch (ClassCircularityError cce) {
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//判断是否需要委托给父类加载器进行加载,delegate属性默认为false,那么delegatedLoad的值就取决于filter的返回值了,filter中是优先加载tomcat的lib下的class文件
//filter方法中根据包名来判断是否需要进行委托加载,默认情况下会返回false.因此delegatedLoad为false
boolean delegateLoad = delegate || filter(name, true);
//因为delegatedLoad为false,那么此时不会委托父加载器去加载,这里其实是没有遵循parent-first的加载机制。
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//调用findClass方法在webapp级别进行加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
//如果还是没有加载到类,并且不采用委托机制的话,则通过父类加载器去加载。
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
既然 Tomcat 不遵循双亲委派机制,那么如果我自己web项目定义一个恶意的HashMap 还有org.apache.catalina.loader.WebappLoader,会不会有风险呢?
不会的,他先从自己的本地缓存和jvm 缓存查找,如果没有找到
第一个调用的是SystemClassLoader,能够加载java 核心类,所以HashMap 是不会被加载到的。
第二个WebAppClassLoader 的 findClass 方法 会有一个check 包权限方法。会检验这个类名的包是不是合法能够加载的。package.definition 这个参数设置哪些包是不能通过WebAppClassLoadr加载的。
#catalina.properties 配置文件里面
package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,\
org.apache.jasper.,org.apache.naming.,org.apache.tomcat.
securityManager.checkPackageDefinition(name.substring(0,i))
public void checkPackageDefinition(String pkg) {
if (pkg == null) {
throw new NullPointerException("package name can't be null");
}
String[] pkgs;
synchronized (packageDefinitionLock) {
if (!packageDefinitionValid) {
String tmpPropertyStr =
AccessController.doPrivileged(
new PrivilegedAction<String>() {
public String run() {
return java.security.Security.getProperty(
"package.definition");
}
}
);
packageDefinition = getPackages(tmpPropertyStr);
packageDefinitionValid = true;
}
pkgs = packageDefinition;
}
for (int i = 0; i < pkgs.length; i++) {
if (pkg.startsWith(pkgs[i]) || pkgs[i].equals(pkg + ".")) {
checkPermission(
new RuntimePermission("defineClassInPackage."+pkg));
break; // No need to continue; only need to check this once
}
}
}
Tomcat 为什么 要打破双亲委派机制?
(1)对于各个 webapp中的 class和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况(同包同民情况,需要隔离类加载器,在不影响上层包的情况下,先找自身,在找父类的),而对于许多应用,需要有共享的lib以便不浪费资源。