一.简介
最近项目中需要分析java文件中依赖的类、Jar包中class文件扫描,通过提取字节码文件中的Constant_Class_info 常量池中的信息实现class 文件依赖分析。使用了ASM 轻量级jar包读取字节码文件,根据JVM虚拟机规范解析常量池信息。
字节码文件类型描述:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>8.0</version>
</dependency>
二.实现
import org.objectweb.asm.ClassReader;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ClassUtil {
/**
* 分析class 文件中Constant_Class_info 信息
* @param classStream
* @return
* @throws IOException
*/
public static List<String> constClassInfo(InputStream classStream) throws IOException {
ClassReader reader = new ClassReader(classStream);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
List<String> infos = new ArrayList<>();
for(int i=1;i<reader.getItemCount();i++) {
int off = reader.getItem(i);
if(off > 0 && reader.readByte(off-1) == 7){
int index = reader.readUnsignedShort(off);
off = reader.getItem(index);
int len = reader.readUnsignedShort(off);
for(int j=0;j<len;j++){
int b = reader.readByte(off+2+j);
bos.write(b);
}
infos.add(bos.toString("utf-8"));
bos.reset();
}
}
return infos;
}
/**
* 分析class 文件中Constant_Utf8_info 常量池信息项
* @param classStream class 字节码流
* @throws IOException
*/
public static List<String> constUtf8Info(InputStream classStream) throws IOException {
ClassReader reader = new ClassReader(classStream);
List<String> infos = new ArrayList<>(128);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
for(int i=1;i<reader.getItemCount();i++) {
int off = reader.getItem(i);
if(off > 0 && reader.readByte(off-1) == 1){
int len = reader.readUnsignedShort(off);
for(int j=0;j<len;j++){
int b = reader.readByte(off+2+j);
bos.write(b);
}
infos.add(bos.toString("utf-8"));
bos.reset();
}
}
return infos;
}
/**
* 扫描zip格式文件
* @param zip zip格式文件流
* @param buf 内部写缓冲区(会调用buf的reset方法重置写入位置,使用时需要特别留意)
* @param exts 扫描文件的后缀名(为空则扫描全部文件)
* @param visitor zip文件项回调
* @throws IOException
*/
public static void zipScan(InputStream zip,ByteArrayOutputStream buf,List<String> exts,ZipEntryVisitor visitor) throws IOException{
ZipInputStream zipIs = zip instanceof ZipInputStream ? (ZipInputStream)zip : new ZipInputStream(zip);
byte[] b = new byte[4096];
for(ZipEntry entry;(entry=zipIs.getNextEntry()) != null;){
if(entry.isDirectory()){
visitor.visit(entry.getName(),null);
}else if(entry.getSize() > 0){
if(exts != null && exts.size() > 0){
boolean flag = false;
for(String ext : exts){
if(entry.getName().endsWith(ext)){
flag = true;
break;
}
}
if(!flag){
continue;
}
}
buf.reset();
for(int len;(len=zipIs.read(b)) > 0;){
buf.write(b,0,len);
}
visitor.visit(entry.getName(),new ByteArrayInputStream(buf.toByteArray()));
zipIs.closeEntry();
}
}
}
/**
* 扫描zip格式文件中的所有class文件
* @param zip zip格式文件流
* @param ignoreZips 忽略不扫描的zip格式文件(.zip .jar .war .ear)
* @param buf 缓冲区
* @param visitor
* @throws IOException
*/
public static void zipClassScan(InputStream zip,List<String> ignoreZips,ByteArrayOutputStream buf,ZipEntryVisitor visitor) throws IOException {
zipScan(zip, buf, Arrays.asList(".zip",".jar",".war",".ear",".class"), new ZipEntryVisitor() {
@Override
public void visit(String name, InputStream is) throws IOException {
if(name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war") || name.endsWith(".ear")){
if(ignoreZips != null && ignoreZips.indexOf(name) >= 0){
return;
}
zipClassScan(is,ignoreZips,buf,visitor);
}else if(name.endsWith(".class")){
visitor.visit(name,is);
}
}
});
}
/**
* 扫描path路径下的class 文件,
* @param path 扫描的文件(可以是路径、.class、.jar、.war、.ear、.zip)
* @param buf 缓冲区
* @param ignoreFiles 忽略不扫描的路径
* @param ignoreZips 忽略不扫描的zip格式文件(.zip .jar .war .ear)
* @param visitor 扫描到.class 文件的回调
* @throws IOException
*/
public static void classScan(File path,List<File> ignoreFiles,List<String> ignoreZips,ByteArrayOutputStream buf,ZipEntryVisitor visitor) throws IOException {
if(path.isFile()){
String name = path.getName();
if(name.endsWith(".class")){
try(InputStream is=new BufferedInputStream(new FileInputStream(path))){
buf.reset();
for(int b;(b=is.read()) != -1;){
buf.write(b);
}
}
visitor.visit(name,new ByteArrayInputStream(buf.toByteArray()));
}else if(name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war") || name.endsWith(".ear")){
if(ignoreZips != null && ignoreZips.indexOf(name) >= 0){
return;
}
try(InputStream is=new BufferedInputStream(new FileInputStream(path))){
zipClassScan(is,ignoreZips,buf,visitor);
}
}
}else{
for(File file : path.listFiles()){
boolean flag = false;
if(ignoreFiles != null && ignoreFiles.size() > 0){
for(File f : ignoreFiles){
if(file.equals(f)){
flag = true;
break;
}
}
}
if(!flag) {
classScan(file, ignoreFiles,ignoreZips, buf, visitor);
}
}
}
}
/**
* 向zip格式文件中添加entryName项
* @param src 需要添加的文件
* @param buf 缓冲区
* @param names 添加项的名字数组
* @param datas 添加项的数据
*/
public static void addEntry(File src,byte[] buf,String[] names,byte[][] datas) throws IOException{
File tmpOut = new File("."+src.getName());
try(ZipInputStream zis=new ZipInputStream(new FileInputStream(src)); ZipOutputStream zos=new ZipOutputStream(new FileOutputStream(tmpOut))){
for(ZipEntry inEnt;(inEnt=zis.getNextEntry()) != null;zis.closeEntry()){
if(inEnt.isDirectory()){
continue;
}
zos.putNextEntry(new ZipEntry(inEnt.getName()));
for(int len;(len=zis.read(buf)) > 0;){
zos.write(buf,0,len);
}
zos.closeEntry();
}
for(int i=0;i<names.length;i++){
zos.putNextEntry(new ZipEntry(names[i]));
zos.write(datas[i]);
}
}catch (IOException e){
tmpOut.delete();
throw e;
}
Files.move(tmpOut.toPath(),src.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
* class 文件常量池的Constant_Class_info 项的访问信息
*/
public interface ClassInfoVisitor{
/**
*
* @param name class文件名
* @param classes 该class文件的Constant_Class_info项的类型信息
*/
void visit(String name,Set<String> classes);
}
/**
* class 文件常量池的Utf8_info 项的访问信息
*/
public interface Utf8InfoVisitor{
/**
*
* @param info Utf8_info常量池项的名称
*/
void visit(String info);
}
/**
* jar/ear/war 格式文件是一种zip 格式的文件
* zip格式压缩文件项的访问
*/
public interface ZipEntryVisitor{
/**
*
* @param name zip格式文件项的名字
* @param is name名字的输入流信息,如果输入项是文件夹,is为空
*/
void visit(String name,InputStream is) throws IOException;
}
}