自定义类加载器与spring的集成

之前也学习过自定义类加载器,通过ClassLoader直接加载需要的类。但实际业务中启动入口常常不可控,比如实际业务中我们常常使用spring对类实例进行管理。如何在spring中集成自定义ClassLoader是需要我们考虑的问题。结合之前项目单机部署的一个方案,即class加密,自定义类加载器解密。因此,我们需要解决两个个问题:
1、自定义类加载器
2、spring与类加载器的集成

[list][*][b]spring与类加载器的集成[/b][/list]
[color=blue]spring与类加载器的集成可以自定义WebContextListener,继承于ContextLoaderListener,在initWebApplicationContext中引入自定义的类加载器[/color],代码如下:
ContainerClassLoader.init();
[color=red]实际上是在当前类加载列表中寻找一个合适的位置插入,通过反射机制插入并更改类加载器父子关系。具体可了解jvm类加载机制及tomcat类加载器机制,两者是有差异的[/color]

[list][*][b]自定义类加载器[/b][/list]
package com.thinkgem.jeesite.test.utils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;

public class ContainerClassLoader extends ClassLoader {

private Map<String, Class<?>> loadedClasses = new HashMap<String, Class<?>>();

private static ContainerClassLoader INSTANCE;

private ContainerClassLoader() {
super(ContainerClassLoader.class.getClassLoader().getParent().getParent());
}

/**
* 初始化
*/
public static void init() {
INSTANCE = new ContainerClassLoader();
try {
INSTANCE.addThisToParentClassLoader(ContainerClassLoader.class
.getClassLoader().getParent());
} catch (Exception e) {
System.err.println("设置classloader到容器中时出现错误!");
}
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (loadedClasses.containsKey(name)) {
return loadedClasses.get(name);
}
return super.loadClass(name, resolve);
}

/**
* 将this替换为指定classLoader的parent ClassLoader
*
* @param classLoader
*/
private void addThisToParentClassLoader(ClassLoader classLoader) throws Exception {
URLClassLoader cl=(URLClassLoader) ClassLoader.getSystemClassLoader();
Field field = ClassLoader.class.getDeclaredField("parent");
field.setAccessible(true);
field.set(classLoader, this);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName="D:/MyTest/classEncrypt/extendPro/"+name.replace(".", File.separator)+".class";
File f=new File(fileName);
if(f.exists()){
FileInputStream fis =null;
ByteArrayOutputStream bos = null;
try {
//将class文件进行解密
fis = new FileInputStream(fileName);
bos = new ByteArrayOutputStream();
encodeAndDecode(fis,bos);
byte[] classByte = bos.toByteArray();
//将字节流变成一个class
return defineClass(name,classByte,0,classByte.length);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return null;
}

/**
* 加密和解密算法
* @param is
* @param os
* @throws Exception
*/
private static void encodeAndDecode(InputStream is,OutputStream os) throws Exception{
int bytes = -1;
while((bytes = is.read())!= -1){
bytes = bytes ^ 0xff;//和0xff进行异或处理
os.write(bytes);
}
}

public static void encodeClass() throws Exception{
String srcPath = "D:/sheungxin/workspaces/jeesite-master/src/main/webapp/WEB-INF/classes/com/thinkgem/jeesite/test/service/HelloWorldService.class";
String desPath = "d:/";//ClassLoaderAttachment.class输出的路径
String desFileName = srcPath.substring(srcPath.lastIndexOf("/")+1);
String desPathFile = desPath + "/" + desFileName;
FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream fos = new FileOutputStream(desPathFile);
//将class进行加密
encodeAndDecode(fis,fos);
fis.close();
fos.close();
}

}

引入自定义的类加载器后,类加载器列表如下:
#tomcat用WebappClassLoader加载应用,其父类加载器为URLClassLoader
WebappClassLoader
context: jeesite
delegate: false
----------> Parent Classloader:java.net.URLClassLoader@3af49f1c
#ClassLoader的子类,用于从指定JAR文件或目录中加载类和资源,ClassLoader只能读取classpath下文件
java.net.URLClassLoader@3af49f1c
com.thinkgem.jeesite.test.utils.ContainerClassLoader@238281e1
sun.misc.Launcher$AppClassLoader@5c647e05
sun.misc.Launcher$ExtClassLoader@68837a77
null

以上为tomcat下web项目的类加载器顺序:[color=blue]WebappClassLoader》URLClassLoader》ContainerClassLoader》AppClassLoader》ExtClassLoader》null[/color]
其中ContainerClassLoader为自定义的类加载器,为什么放在该位置?WebappClassLoader也有parent私有属性,在构造时赋值,与基类ClassLoader一样,插入自定义类加载器,改变类加载器父子关系的时两个私有属性都需要调整(没修改过来,后续再测试下)。因为不好修改且猜测WebappClassLoader》URLClassLoader放在一起比较好,暂时放在URLClassLoader后面。这样就可以成功加载加密的类,[color=red]但需要注意加密后的类不能放在tomcat下,因为会校验class有效性。[/color]
HelloWorldService helloWorldService=new HelloWorldService();
helloWorldService.say();

通过上面测试代码,可以正常输出“Hello World!”
但还存在一个问题,如何把加载后的类交给spring进行管理?经测验通过xml配置的方式,bean已成功加载,已交由spring容器管理。
<bean id="helloWorldService" class="com.thinkgem.jeesite.test.service.HelloWorldService"/>

但通常使用注解的方式,[color=red]通过component-scan进行代码扫描,经测试,无法扫描到项目外的class文件,因为外部class并不在当前classpath下[/color],具体可参见源码:
ClassPathBeanDefinitionScanner.doScan
ClassPathScanningCandidateComponentProvider.findCandidateComponents

[color=red]因此需要把外部类路径加入当前classpath,可以自定义classloader,继承URLClassLoader,把外部类路径加入URL数组中,代码如下:[/color]
package com.thinkgem.jeesite.test.utils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class DefinedURLClassLoader extends URLClassLoader{

public DefinedURLClassLoader(URL[] urls) {
super(urls);
}

public DefinedURLClassLoader(URL[] urls, ClassLoader classLoader) {
super(urls, classLoader);
try {
//添加外部类路径
this.addURL(new URL("file:/D:/MyTest/classEncrypt/extendPro/"));
init();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}

public void init() {
try {
addThisToParentClassLoader(DefinedURLClassLoader.class
.getClassLoader().getParent());
} catch (Exception e) {
System.err.println("设置classloader到容器中时出现错误!");
}
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}

/**
* 将this替换为指定classLoader的parent ClassLoader
*
* @param classLoader
*/
private void addThisToParentClassLoader(ClassLoader classLoader) throws Exception {
Field field = ClassLoader.class.getDeclaredField("parent");
field.setAccessible(true);
field.set(classLoader, this);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName="D:/MyTest/classEncrypt/extendPro/"+name.replace(".", File.separator)+".class";
File f=new File(fileName);
if(f.exists()){
FileInputStream fis =null;
ByteArrayOutputStream bos = null;
try {
//将class文件进行解密
fis = new FileInputStream(fileName);
bos = new ByteArrayOutputStream();
encodeAndDecode(fis,bos);
byte[] classByte = bos.toByteArray();
//将字节流变成一个class
return defineClass(name,classByte,0,classByte.length);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return null;
}

/**
* 加密和解密算法
* @param is
* @param os
* @throws Exception
*/
private static void encodeAndDecode(InputStream is,OutputStream os) throws Exception{
int bytes = -1;
while((bytes = is.read())!= -1){
bytes = bytes ^ 0xff;//和0xff进行异或处理
os.write(bytes);
}
}

}

在自定义监听器WebContextListener中调用:
URLClassLoader ucl=(URLClassLoader) this.getClass().getClassLoader().getParent();
DefinedURLClassLoader durl=new DefinedURLClassLoader(ucl.getURLs(), ucl.getParent());

[color=blue]把自定义的DefinedURLClassLoader放在之前ContainerClassLoader的位置,类加载顺序如下:[/color]
WebappClassLoader
context: jeesite
delegate: false
----------> Parent Classloader:java.net.URLClassLoader@3af49f1c
#ClassLoader的子类,用于从指定JAR文件或目录中加载类和资源,ClassLoader只能读取classpath下文件
java.net.URLClassLoader@3af49f1c
com.thinkgem.jeesite.test.utils.DefinedURLClassLoader@238281e1
sun.misc.Launcher$AppClassLoader@5c647e05
sun.misc.Launcher$ExtClassLoader@68837a77
null

[color=blue]两者的区别在于继承的类不同,前者继承ClassLoader,后者继承URLClassLoader(URLClassLoader extends SecureClassLoader extends ClassLoader),后者好处在于可以自定义URL地址,同时具体类加载器的解析功能。[/color]下面可以打印出更改后的url,如下:
for(URL u:durl.getURLs()){
System.out.println(u.toString());
}

[color=blue]输出如下:file:/D:/MyTest/classEncrypt/extendPro/[/color]
可以看见新增加的路径,这时spring扫描时就可以找到外部类文件,可以追踪ClassPathScanningCandidateComponentProvider类,在Resource中可以看到需要加载的类文件
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + "/" + this.resourcePattern;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
...
}

[color=red]重新启动服务,同样失败,错误如下:[/color]
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [D:\MyTest\classEncrypt\extendPro\com\thinkgem\jeesite\test\service\HelloWorldService.class]; nested exception is java.lang.ArrayIndexOutOfBoundsException: 658
at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:303)
at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:248)
at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:87)
...
Caused by: java.lang.ArrayIndexOutOfBoundsException: 658
at org.springframework.asm.ClassReader.<init>(ClassReader.java:183)
at org.springframework.asm.ClassReader.<init>(ClassReader.java:153)
at org.springframework.asm.ClassReader.<init>(ClassReader.java:426)
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:53)
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:98)
at org.springframework.core.type.classreading.CachingMetadataReaderFactory.getMetadataReader(CachingMetadataReaderFactory.java:102)
...
org.apache.tomcat.util.bcel.classfile.ClassFormatException: It is not a Java .class file
...
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
SimpleMetadataReader》ClassReader

由报错可以看出HelloWorldService.class解析失败,定位到183行: switch (b[index]) ,[color=red]原因在于class文件被加密,spring代码扫描是通过对编译好的字节码进行处理,ClassReader是字节码访问类,无法正确编译加密后的class文件,[/color]因此就报了以上错误。错误定位源码如下:
public ClassReader(final byte[] b, final int off, final int len) {
this.b = b;
// checks the class version
/* SPRING PATCH: REMOVED FOR FORWARD COMPATIBILITY WITH JDK 9
if (readShort(off + 6) > Opcodes.V1_8) {
throw new IllegalArgumentException();
}
*/
// parses the constant pool
items = new int[readUnsignedShort(off + 8)];
int n = items.length;
strings = new String[n];
int max = 0;
int index = off + 10;
for (int i = 1; i < n; ++i) {
items[i] = index + 1;
int size;
switch (b[index]) {
case ClassWriter.FIELD:
case ClassWriter.METH:
case ClassWriter.IMETH:
case ClassWriter.INT:
case ClassWriter.FLOAT:
case ClassWriter.NAME_TYPE:
case ClassWriter.INDY:
size = 5;
break;
case ClassWriter.LONG:
case ClassWriter.DOUBLE:
size = 9;
++i;
break;
case ClassWriter.UTF8:
size = 3 + readUnsignedShort(index + 1);
if (size > max) {
max = size;
}
break;
case ClassWriter.HANDLE:
size = 4;
break;
// case ClassWriter.CLASS:
// case ClassWriter.STR:
// case ClassWriter.MTYPE
default:
size = 3;
break;
}
index += size;
}
maxStringLength = max;
// the class header information starts just after the constant pool
header = index;
}
...
private static byte[] readClass(final InputStream is, boolean close)
throws IOException {
if (is == null) {
throw new IOException("Class not found");
}
try {
byte[] b = new byte[is.available()];
int len = 0;
while (true) {
int n = is.read(b, len, b.length - len);
if (n == -1) {
if (len < b.length) {
byte[] c = new byte[len];
System.arraycopy(b, 0, c, 0, len);
b = c;
}
return b;
}
len += n;
if (len == b.length) {
int last = is.read();
if (last < 0) {
return b;
}
byte[] c = new byte[b.length + 1000];
System.arraycopy(b, 0, c, 0, len);
c[len++] = (byte) last;
b = c;
}
}
} finally {
if (close) {
is.close();
}
}
}

[list][*][b]总结[/b][/list]
通过以上实验,我们可以达到以下目的:
1、自定义类加载器加载指定外部class
2、自定义类加载器与spring集成,加载的实例交由spring进行管理

未完美解决问题:[color=blue]加密的外部class文件,通过xml配置的方式可以成功加载并交由spring容器进行管理;但注解、spring代码扫描(component-scan)的方式无法正确扫描加密后的类,需要解决ClassReader.readClass(),对读取到的字节数组进行处理后再交由ClassReader编译[/color],整个类调用流程如下:
ComponentScanBeanDefinitionParser.parse》ClassPathBeanDefinitionScanner.doScan》ClassPathScanningCandidateComponentProvider.findCandidateComponents》CachingMetadataReaderFactory.getMetadataReader》SimpleMetadataReaderFactory.getMetadataReader》SimpleMetadataReader(Resource resource, ClassLoader classLoader)》ClassReader(final InputStream is)》readClass(final InputStream is, boolean close)》ClassReader(final byte[] b)》ClassReader(final byte[] b, final int off, final int len)

[color=blue]如果自定义ClassReader,下面我们就需要寻找spring可扩展点,崩溃中...有兴趣的可以试试[/color]

[list][*][b]尝试的其它方案[/b][/list]
//动态加载bean到spring容器中,通过XmlBeanDefinitionReader的方式,
ApplicationContext context=new ClassPathXmlApplicationContext(new String[]{"spring-context.xml"});
//web环境中获取方式:context=ContextLoader.getCurrentWebApplicationContext();
XmlBeanDefinitionReader xmlBeanDefinitionReader=new XmlBeanDefinitionReader((BeanDefinitionRegistry)context.getAutowireCapableBeanFactory());
xmlBeanDefinitionReader.setEntityResolver(new ResourceEntityResolver(context));
xmlBeanDefinitionReader.loadBeanDefinitions("spring/spring-extend.xml");
//xml文件必须在classpath中,源码中文件流代码:is = ClassLoader.getSystemResourceAsStream(this.path);

DefaultListableBeanFactory beanFactory=(DefaultListableBeanFactory) webApplicationContext.getAutowireCapableBeanFactory();
beanFactory.setBeanClassLoader(durl);


[list][*][b]待尝试方案[/b][/list]
有人提到可以使用java agent,有待尝试...

[list][*][b]参考文档[/b][/list]
spring启动component-scan类扫描加载过程---源码分析
[url]http://blog.csdn.net/u014723529/article/details/41958841[/url]

动态修改当前ClassLoader
[url]http://bingobird.iteye.com/blog/606116[/url]

额外JAR包的加载问题
[url]http://www.inter12.org/archives/702[/url]
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
集成 Spring Security可以帮助我们实现应用程序的身份验证和授权功能。下面是一些集成 Spring Security 的步骤: 1. 添加 Spring Security 依赖:在项目的构建文件中,如 Maven 的 pom.xml 或 Gradle 的 build.gradle 文件中,添加 Spring Security 的依赖。 2. 配置 Spring Security:在项目的配置文件中,如 application.properties 或 application.yml 文件中,配置 Spring Security 的相关属性,如登录路径、注销路径等。 3. 创建用户认证服务:实现 UserDetailsService 接口,并重写其 loadUserByUsername 方法,用于根据用户名加载用户信息。可以从数据库、内存或其他认证源加载用户信息。 4. 配置密码加密:为了安全起见,建议将用户密码进行加密存储。可以使用 Spring Security 提供的 PasswordEncoder 接口进行密码加密和验证。 5. 配置身份验证管理器:创建 AuthenticationManager Bean,并将用户认证服务和密码加密器注入其中。 6. 配置安全规则:通过配置继承 WebSecurityConfigurerAdapter 并重写 configure 方法,配置安全规则,如哪些路径需要身份验证、哪些路径需要特定角色才能访问等。 7. 自定义登录页面:如果需要自定义登录页面,可以创建一个登录页面,并在安全配置中指定该页面。 8. 保护资源:根据需求,可以配置哪些资源需要授权才能访问,如静态资源、API 接口等。 以上是集成 Spring Security 的一般步骤,具体的实现会根据项目的需求和架构有所不同。希望对你有所帮助!如果还有其他问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值