spring-loaded TypeDiffComputer 源码
spring-loaded TypeDiffComputer 代码
文件路径:/springloaded/src/main/java/org/springsource/loaded/TypeDiffComputer.java
/*
* Copyright 2010-2012 VMware and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springsource.loaded;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.MultiANewArrayInsnNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
/**
* Compute the differences between two versions of a type as a series of deltas. Entry point is the computeDifferences
* method.
*
* @author Andy Clement
* @since 0.5.0
*/
public class TypeDiffComputer implements Opcodes {
public static TypeDelta computeDifferences(byte[] oldbytes, byte[] newbytes) {
ClassNode oldClassNode = new ClassNode();
new ClassReader(oldbytes).accept(oldClassNode, 0);
ClassNode newClassNode = new ClassNode();
new ClassReader(newbytes).accept(newClassNode, 0);
TypeDelta delta = computeDelta(oldClassNode, newClassNode);
return delta;
}
private static TypeDelta computeDelta(ClassNode oldClassNode, ClassNode newClassNode) {
// The type itself: (int version, int access, String name, String signature, String superName, String[] interfaces) {
TypeDelta td = new TypeDelta();
computeTypeDelta(oldClassNode, newClassNode, td);
computeFieldDelta(oldClassNode, newClassNode, td);
computeMethodDelta(oldClassNode, newClassNode, td);
// TODO delta: implement the rest of computeDelta. These methods from ClassVisitor should help in knowing what is left to do:
// public void visitSource(String source, String debug) {
// public void visitOuterClass(String owner, String name, String desc) {
// public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// public void visitAttribute(Attribute attr) {
// public void visitInnerClass(String name, String outerName, String innerName, int access) {
// public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
// public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// public void visitEnd() {
return td;
}
@SuppressWarnings("unchecked")
private static void computeMethodDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
List<MethodNode> nMethods = newClassNode.methods;
List<MethodNode> oMethods = new ArrayList<MethodNode>(oldClassNode.methods);
// Going through the new methods and comparing them to the old
if (nMethods != null) {
for (MethodNode nMethod : nMethods) {
MethodNode found = null;
for (MethodNode oMethod : oMethods) {
if (oMethod.name.equals(nMethod.name) && oMethod.desc.equals(nMethod.desc)) { // TODO modifiers compared?
found = oMethod;
computeAnyMethodDifferences(oMethod, nMethod, td);
}
}
if (found == null) {
td.addNewMethod(nMethod);
}
else {
oMethods.remove(found);
}
}
}
for (MethodNode lostMethod : oMethods) {
td.addLostMethod(lostMethod);
}
}
@SuppressWarnings("unchecked")
private static void computeFieldDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
// int oSize = oldClassNode.fields.size();
int nSize = newClassNode.fields.size();
// Take a copy as we are going to delete entries in the next loop
List<FieldNode> oFields = new ArrayList<FieldNode>(oldClassNode.fields);
// Going through the new fields comparing them to the old
for (int n = 0; n < nSize; n++) {
FieldNode nField = (FieldNode) newClassNode.fields.get(n);
FieldNode found = null;
for (FieldNode oField : oFields) {
if (oField.name.equals(nField.name)) {
// found it!
found = oField;
// is it exactly the same?
computeAnyFieldDifferences(oField, nField, td);
}
}
if (found == null) {
// this is a new field
td.addNewField(nField);
}
else {
oFields.remove(found);
}
}
// Those left in oFields were not in nFields so have been removed!
for (FieldNode lostField : oFields) {
td.addLostField(lostField);
}
}
/**
* Check the properties of the field - if they have changed at all then record what kind of change for the field.
* Thinking the type delta should have a map from names to a delta describing (capturing) the change.
*/
@SuppressWarnings("unchecked")
private static void computeAnyFieldDifferences(FieldNode oField, FieldNode nField, TypeDelta td) {
// Want to record things that are different between these two fields...
FieldDelta fd = new FieldDelta(oField.name);
if (oField.access != nField.access) {
// access changed
fd.setAccessChanged(oField.access, nField.access);
}
if (!oField.desc.equals(nField.desc)) {
// type changed
fd.setTypeChanged(oField.desc, nField.desc);
}
String annotationChange = compareAnnotations(oField.invisibleAnnotations, nField.invisibleAnnotations);
annotationChange = annotationChange + compareAnnotations(oField.visibleAnnotations, nField.visibleAnnotations);
if (annotationChange.length() != 0) {
fd.setAnnotationsChanged(annotationChange);
}
if (fd.hasAnyChanges()) {
// it needs recording
td.addChangedField(fd);
}
}
/**
* Determine if there any differences between the methods supplied. A MethodDelta object is built to record any
* differences and stored against the type delta.
*
* @param oMethod 'old' method
* @param nMethod 'new' method
* @param td the type delta where changes are currently being accumulated
*/
private static void computeAnyMethodDifferences(MethodNode oMethod, MethodNode nMethod, TypeDelta td) {
MethodDelta md = new MethodDelta(oMethod.name, oMethod.desc);
if (oMethod.access != nMethod.access) {
md.setAccessChanged(oMethod.access, nMethod.access);
}
// TODO annotations
InsnList oInstructions = oMethod.instructions;
InsnList nInstructions = nMethod.instructions;
if (oInstructions.size() != nInstructions.size()) {
md.setInstructionsChanged(oInstructions.toArray(), nInstructions.toArray());
}
else {
// TODO Just interested in constructors right now - should add others
if (oMethod.name.charAt(0) == '<') {
String oInvokeSpecialDescriptor = null;
String nInvokeSpecialDescriptor = null;
int oUninitCount = 0;
int nUninitCount = 0;
boolean codeChange = false;
for (int i = 0, max = oInstructions.size(); i < max; i++) {
AbstractInsnNode oInstruction = oInstructions.get(i);
AbstractInsnNode nInstruction = nInstructions.get(i);
if (!codeChange) {
if (!sameInstruction(oInstruction, nInstruction)) {
codeChange = true;
}
}
if (oInstruction.getType() == AbstractInsnNode.TYPE_INSN) {
if (oInstruction.getOpcode() == Opcodes.NEW) {
oUninitCount++;
}
}
if (nInstruction.getType() == AbstractInsnNode.TYPE_INSN) {
if (nInstruction.getOpcode() == Opcodes.NEW) {
nUninitCount++;
}
}
if (oInstruction.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode mi = (MethodInsnNode) oInstruction;
if (mi.getOpcode() == INVOKESPECIAL && mi.name.equals("<init>")) {
if (oUninitCount == 0) {
// this is the one!
oInvokeSpecialDescriptor = mi.desc;
}
else {
oUninitCount--;
}
}
}
if (nInstruction.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode mi = (MethodInsnNode) nInstruction;
if (mi.getOpcode() == INVOKESPECIAL && mi.name.equals("<init>")) {
if (nUninitCount == 0) {
// this is the one!
nInvokeSpecialDescriptor = mi.desc;
}
else {
nUninitCount--;
}
}
}
}
// Has the invokespecial changed?
if (oInvokeSpecialDescriptor == null) {
if (nInvokeSpecialDescriptor != null) {
md.setInvokespecialChanged(oInvokeSpecialDescriptor, nInvokeSpecialDescriptor);
}
}
else {
if (!oInvokeSpecialDescriptor.equals(nInvokeSpecialDescriptor)) {
md.setInvokespecialChanged(oInvokeSpecialDescriptor, nInvokeSpecialDescriptor);
}
}
if (codeChange) {
md.setCodeChanged(oInstructions.toArray(), nInstructions.toArray());
}
}
}
if (md.hasAnyChanges()) {
// it needs recording
td.addChangedMethod(md);
}
}
private static boolean sameInstruction(AbstractInsnNode o, AbstractInsnNode n) {
if (o.getType() != o.getType() || o.getOpcode() != n.getOpcode()) {
return false;
}
switch (o.getType()) {
case (AbstractInsnNode.INSN):
if (!sameInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.INT_INSN):
if (!sameIntInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.VAR_INSN):
if (!sameVarInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.TYPE_INSN):
if (!sameTypeInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.FIELD_INSN):
if (!sameFieldInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.METHOD_INSN):
if (!sameMethodInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.INVOKE_DYNAMIC_INSN):
if (!sameInvokeDynamicInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.JUMP_INSN):
if (!sameJumpInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.LABEL):
if (!sameLabelNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.LDC_INSN):
if (!sameLdcInsnNode(o, n)) {
return false;
}
break;
case (AbstractInsnNode.IINC_INSN):
if (!sameIincInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.TABLESWITCH_INSN):
if (!sameTableSwitchInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.LOOKUPSWITCH_INSN):
if (!sameLookupSwitchInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.MULTIANEWARRAY_INSN):
if (!sameMultiANewArrayInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.FRAME):
if (!sameFrameInsn(o, n)) {
return false;
}
break;
case (AbstractInsnNode.LINE):
if (!sameLineNumberNode(o, n)) {
return false;
}
break;
default:
throw new IllegalStateException("nyi " + o.getType());
}
return true;
}
private static boolean sameFrameInsn(AbstractInsnNode o, AbstractInsnNode n) {
// given that these nodes are computed based on everything else. if everything else is the same then these
// must be the same. A full comparison could be a little ugly as different frames can be equivalent (maybe
// the compiler produces an incremental frame on one run then a full frame on the next).
return true;
}
private static boolean sameMultiANewArrayInsn(AbstractInsnNode o, AbstractInsnNode n) {
if (!(n instanceof MultiANewArrayInsnNode)) {
return false;
}
MultiANewArrayInsnNode mnao = (MultiANewArrayInsnNode) o;
MultiANewArrayInsnNode mnan = (MultiANewArrayInsnNode) n;
if (!mnao.desc.equals(mnan.desc)) {
return false;
}
if (mnao.dims != mnan.dims) {
return false;
}
return true;
}
@SuppressWarnings("unchecked")
private static boolean sameLookupSwitchInsn(AbstractInsnNode o, AbstractInsnNode n) {
if (!(n instanceof LookupSwitchInsnNode)) {
return false;
}
LookupSwitchInsnNode lsio = (LookupSwitchInsnNode) o;
LookupSwitchInsnNode lsin = (LookupSwitchInsnNode) n;
if (sameLabels(lsio.dflt, lsin.dflt)) {
return false;
}
List<Integer> keyso = lsio.keys;
List<Integer> keysn = lsin.keys;
if (keyso.size() != keysn.size()) {
return false;
}
for (int i = 0, max = keyso.size(); i < max; i++) {
if (keyso.get(i) != keysn.get(i)) {
return false;
}
}
List<LabelNode> labelso = lsio.labels;
List<LabelNode> labelsn = lsin.labels;
if (labelso.size() != labelsn.size()) {
return false;
}
for (int i = 0, max = labelso.size(); i < max; i++) {
if (!sameLabelNode(labelso.get(i), labelsn.get(i))) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
private static boolean sameTableSwitchInsn(AbstractInsnNode o, AbstractInsnNode n) {
if (!(n instanceof TableSwitchInsnNode)) {
return false;
}
TableSwitchInsnNode tsio = (TableSwitchInsnNode) o;
TableSwitchInsnNode tsin = (TableSwitchInsnNode) n;
if (sameLabels(tsio.dflt, tsin.dflt)) {
return false;
}
if (tsio.min != tsin.min) {
return false;
}
if (tsio.max != tsin.max) {
return false;
}
List<LabelNode> labelso = tsio.labels;
List<LabelNode> labelsn = tsin.labels;
if (labelso.size() != labelsn.size()) {
return false;
}
for (int i = 0, max = labelso.size(); i < max; i++) {
if (!sameLabelNode(labelso.get(i), labelsn.get(i))) {
return false;
}
}
return true;
}
private static boolean sameLabels(LabelNode lno, LabelNode lnn) {
// TODO implement?
return false;
}
private static boolean sameFieldInsn(AbstractInsnNode o, AbstractInsnNode n) {
FieldInsnNode oi = (FieldInsnNode) o;
if (!(n instanceof FieldInsnNode)) {
return false;
}
FieldInsnNode ni = (FieldInsnNode) n;
return oi.name.equals(ni.name) && oi.desc.equals(ni.desc) && oi.owner.equals(ni.owner);
}
private static boolean sameMethodInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
MethodInsnNode oi = (MethodInsnNode) o;
if (!(n instanceof MethodInsnNode)) {
return false;
}
MethodInsnNode ni = (MethodInsnNode) n;
return oi.name.equals(ni.name) && oi.desc.equals(ni.desc) && oi.owner.equals(ni.owner);
}
private static boolean sameInvokeDynamicInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
InvokeDynamicInsnNode oi = (InvokeDynamicInsnNode) o;
if (!(n instanceof InvokeDynamicInsnNode)) {
return false;
}
InvokeDynamicInsnNode ni = (InvokeDynamicInsnNode) n;
if (!oi.name.equals(ni.name) || !oi.desc.equals(ni.desc)) {
return false;
}
if (!sameBsm(oi.bsm, ni.bsm)) {
return false;
}
Object[] oArgs = oi.bsmArgs;
Object[] nArgs = ni.bsmArgs;
if ((oArgs == null && nArgs != null) || (nArgs == null && oArgs != null)) {
return false;
}
if (oArgs.length != nArgs.length) {
return false;
}
for (int i = 0; i < oArgs.length; i++) {
if (!oArgs[i].equals(nArgs[i])) {
return false;
}
}
return true;
}
private static boolean sameBsm(Handle o, Handle n) {
return (o.equals(n));
}
private static boolean sameVarInsn(AbstractInsnNode o, AbstractInsnNode n) {
VarInsnNode oi = (VarInsnNode) o;
if (!(n instanceof VarInsnNode)) {
return false;
}
VarInsnNode ni = (VarInsnNode) n;
return oi.var == ni.var;
}
private static boolean sameInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
InsnNode oi = (InsnNode) o;
if (!(n instanceof InsnNode)) {
return false;
}
InsnNode ni = (InsnNode) n;
return oi.getOpcode() == ni.getOpcode();
}
private static boolean sameJumpInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
// JumpInsnNode oJumpInsnNode = (JumpInsnNode) o;
if (!(n instanceof JumpInsnNode)) {
return false;
}
// JumpInsnNode nJumpInsnNode = (JumpInsnNode) n;
// TODO tricky to compare destinations when captured as labels with no exposed identifier/position
return true;
}
private static boolean sameLdcInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
LdcInsnNode oi = (LdcInsnNode) o;
if (!(n instanceof LdcInsnNode)) {
return false;
}
LdcInsnNode ni = (LdcInsnNode) n;
Object ocst = oi.cst;
if (ocst instanceof Integer) {
if (!(ni.cst instanceof Integer)) {
return false;
}
return ((Integer) ocst).equals(ni.cst);
}
if (ocst instanceof Float) {
if (!(ni.cst instanceof Float)) {
return false;
}
return ((Float) ocst).equals(ni.cst);
}
if (ocst instanceof Long) {
if (!(ni.cst instanceof Long)) {
return false;
}
return ((Long) ocst).equals(ni.cst);
}
if (ocst instanceof Double) {
if (!(ni.cst instanceof Double)) {
return false;
}
return ((Double) ocst).equals(ni.cst);
}
if (ocst instanceof String) {
if (!(ni.cst instanceof String)) {
return false;
}
return ((String) ocst).equals(ni.cst);
}
// must be Type
return ((Type) ocst).equals(ni.cst);
}
private static boolean sameIntInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
IntInsnNode oi = (IntInsnNode) o;
if (!(n instanceof IntInsnNode)) {
return false;
}
IntInsnNode ni = (IntInsnNode) n;
return oi.operand == ni.operand;
}
private static boolean sameLineNumberNode(AbstractInsnNode o, AbstractInsnNode n) {
LineNumberNode oi = (LineNumberNode) o;
if (!(n instanceof LineNumberNode)) {
return false;
}
LineNumberNode ni = (LineNumberNode) n;
return oi.line == ni.line;
// TODO check oi.start?
}
private static boolean sameIincInsn(AbstractInsnNode o, AbstractInsnNode n) {
IincInsnNode oi = (IincInsnNode) o;
if (!(n instanceof IincInsnNode)) {
return false;
}
IincInsnNode ni = (IincInsnNode) n;
return oi.var == ni.var && oi.incr == ni.incr;
}
private static boolean sameTypeInsn(AbstractInsnNode o, AbstractInsnNode n) {
TypeInsnNode oi = (TypeInsnNode) o;
if (!(n instanceof TypeInsnNode)) {
return false;
}
TypeInsnNode ni = (TypeInsnNode) n;
return oi.desc.equals(ni.desc);
}
/**
* Compare two labels to check they are the same.
*
* @param o 'old' label
* @param n 'new' label
* @return true if they are different
*/
private static boolean sameLabelNode(AbstractInsnNode o, AbstractInsnNode n) {
// LabelNode oi = (LabelNode) o;
if (!(n instanceof LabelNode)) {
return false;
}
// LabelNode ni = (LabelNode) n;
// TODO tricky to get right. Unfortunately the positions aren't always available - and we can't check if they are, we have to call the
// getOffset() method on label and catch an exception if they aren't
return true;
}
private static String compareAnnotations(List<AnnotationNode> oldAnnos, List<AnnotationNode> newAnnos) {
if (oldAnnos == null) {
if (newAnnos == null) {
return "";
}
oldAnnos = Collections.emptyList();
}
if (newAnnos == null) {
newAnnos = Collections.emptyList();
}
StringBuilder diff = new StringBuilder();
// Which have been removed
for (AnnotationNode o : oldAnnos) {
boolean found = false;
String oFormatted = Utils.annotationNodeFormat(o);
for (AnnotationNode n : newAnnos) {
String nFormatted = Utils.annotationNodeFormat(n);
if (oFormatted.equals(nFormatted)) {
found = true;
break;
}
}
if (!found) {
diff.append("-").append(oFormatted);
}
}
// Which have been added
for (AnnotationNode n : newAnnos) {
boolean found = false;
String nFormatted = Utils.annotationNodeFormat(n);
for (AnnotationNode o : oldAnnos) {
String oFormatted = Utils.annotationNodeFormat(o);
if (oFormatted.equals(nFormatted)) {
found = true;
break;
}
}
if (!found) {
diff.append("+").append(nFormatted);
}
}
return diff.toString();
}
@SuppressWarnings("unchecked")
private static void computeTypeDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
// if (oldClassNode.version != newClassNode.version) {
// td.setTypeVersionChange(oldClassNode.version, newClassNode.version);
// }
if (oldClassNode.access != newClassNode.access) {
// Is it only because of 0x20000 - that appears to represent Deprecated!
if ((oldClassNode.access & 0xffff) != (newClassNode.access & 0xffff)) {
td.setTypeAccessChange(oldClassNode.access, newClassNode.access);
}
}
if (!oldClassNode.name.equals(newClassNode.name)) {
td.setTypeNameChange(oldClassNode.name, newClassNode.name);
}
// if (oldClassNode.signature == null) {
// if (newClassNode.signature != null) {
// td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
// }
// } else if (newClassNode.signature == null) {
// if (oldClassNode.signature != null) {
// td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
// }
// } else if (!oldClassNode.signature.equals(newClassNode.signature)) {
// td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
// }
if (oldClassNode.superName == null) {
if (newClassNode.superName != null) {
td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
}
}
else if (newClassNode.superName == null) {
if (oldClassNode.superName != null) {
td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
}
}
else if (!oldClassNode.superName.equals(newClassNode.superName)) {
td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
}
if (oldClassNode.interfaces.size() == 0) {
if (newClassNode.interfaces.size() != 0) {
td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
}
}
else if (newClassNode.interfaces.size() == 0) {
if (oldClassNode.interfaces.size() != 0) {
td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
}
}
else {
if (oldClassNode.interfaces.size() != newClassNode.interfaces.size()) {
td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
}
HashSet<String> oldInterfaceSet = new HashSet<String>(oldClassNode.interfaces);
HashSet<String> newInterfaceSet = new HashSet<String>(newClassNode.interfaces);
if (!oldInterfaceSet.equals(newInterfaceSet)) { // TODO expensive? keep the interfaces list sorted instead?
td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
}
}
}
}
相关信息
相关文章
spring-loaded AbstractMember 源码
spring-loaded AnyTypePattern 源码
spring-loaded ChildClassLoader 源码
spring-loaded ConstantPoolChecker 源码
spring-loaded ConstantPoolChecker2 源码
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦