zoukankan      html  css  js  c++  java
  • 美团热修复Robust-源码篇

      上一篇主要分析了Robust的使用方法,这一篇就来总结一下Robust的源码分析。

      我个人倾向于将Robust框架分为两个部分,自动插入代码和动态加载Patch。

    一、Robust源码分析

      目前我的分析将Robust动态加载分为两个部分,一部分是插桩后的代码逻辑,一部分是拉取Patch的逻辑。

      我们首先来看插桩后的代码(这里面套用的是官方的代码,可能有些过时了)

      插桩前

    public long getIndex() {
        return 100;
    }

      插桩后

    public static ChangeQuickRedirect changeQuickRedirect;
        public long getIndex() {
            if(changeQuickRedirect != null) {
                //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
                if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
                    return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
                }
            }
        return 100L;
    }

      我们可以看到Robust为我们的类添加了一个静态的ChangeQuickRedirect对象,我们可以看到当ChangeQuickRedirect为空时,证明此时没有补丁,走原逻辑。当它不为空时,我们可以看到它调用了PatchProxy中的isSupport方法和accessDispatch方法。我们具体来看一下PatchProxy中的这两个方法。

      

     1   public static boolean isSupport(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
     2         //Robust补丁优先执行,其他功能靠后
     3         if (changeQuickRedirect == null) {
     4             //不执行补丁,轮询其他监听者
     5             if (registerExtensionList == null || registerExtensionList.isEmpty()) {
     6                 return false;
     7             }
     8             for (RobustExtension robustExtension : registerExtensionList) {
     9                 if (robustExtension.isSupport(new RobustArguments(paramsArray, current, isStatic, methodNumber, paramsClassTypes, returnType))) {
    10                     robustExtensionThreadLocal.set(robustExtension);
    11                     return true;
    12                 }
    13             }
    14             return false;
    15         }
    16         String classMethod = getClassMethod(isStatic, methodNumber);
    17         if (TextUtils.isEmpty(classMethod)) {
    18             return false;
    19         }
    20         Object[] objects = getObjects(paramsArray, current, isStatic);
    21         try {
    22             return changeQuickRedirect.isSupport(classMethod, objects);
    23         } catch (Throwable t) {
    24             return false;
    25         }
    26     }

      我们可以看到第22行,它调用了changeQuickRedirect.isSupport方法,这个changeQuickRedirect便是我们注入的对象。

      我们接下来再看accessDispatch方法

     1  public static Object accessDispatch(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
     2 
     3         if (changeQuickRedirect == null) {
     4             RobustExtension robustExtension = robustExtensionThreadLocal.get();
     5             robustExtensionThreadLocal.remove();
     6             if (robustExtension != null) {
     7                 notify(robustExtension.describeSelfFunction());
     8                 return robustExtension.accessDispatch(new RobustArguments(paramsArray, current, isStatic, methodNumber, paramsClassTypes, returnType));
     9             }
    10             return null;
    11         }
    12         String classMethod = getClassMethod(isStatic, methodNumber);
    13         if (TextUtils.isEmpty(classMethod)) {
    14             return null;
    15         }
    16         notify(Constants.PATCH_EXECUTE);
    17         Object[] objects = getObjects(paramsArray, current, isStatic);
    18         return changeQuickRedirect.accessDispatch(classMethod, objects);

      可以看到第18行调用了changeQuickRedirect的accseeDispatch方法。

      注入后的代码我们先看到这里,我们接下来看一看,我们拉取Patch的代码

     1   new PatchExecutor(getApplicationContext(), new PatchManpulateImp(), new RobustCallBack() {
     2                     @Override
     3                     public void onPatchListFetched(boolean result, boolean isNet, List<Patch> patches) {
     4                         Log.e("error-hot", "打印 onPatchListFetched:" + "isNet=" + isNet );
     5                     }
     6                     @Override
     7                     public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
     8                         Log.e("error-hot", "打印 onPatchFetched:" + "result=" + result+"isNet="+isNet + "--->" + "patch=" + patch);
     9                     }
    10                     @Override
    11                     public void onPatchApplied(boolean result, Patch patch) {
    12                         Log.e("error-hot", "打印 onPatchApplied:" + "result=" + result + "--->" + "patch=" + patch);
    13                     }
    14                     @Override
    15                     public void logNotify(String log, String where) {
    16                         Log.e("error-hot", "打印 logNotify:" + "log=" + log + "--->" + "where=" + where);
    17                     }
    18                     @Override
    19                     public void exceptionNotify(Throwable throwable, String where) {
    20                         Log.e("error-hot", "打印 exceptionNotify:" + "throwable=" + throwable.toString() + "--->" + "where=" + where);
    21                     }
    22                 }).start();

      进入PatchExecutor类中看一看,我们可以发现它继承了一个线程,那么直接去run方法看一下

     1    @Override
     2     public void run() {
     3         try {
     4             //拉取补丁列表
     5             List<Patch> patches = fetchPatchList();
     6             //应用补丁列表
     7             applyPatchList(patches);
     8         } catch (Throwable t) {
     9             Log.e("robust", "PatchExecutor run", t);
    10             robustCallBack.exceptionNotify(t, "class:PatchExecutor,method:run,line:36");
    11         }
    12     }

      可以看到run方法中做了两件事,拉取补丁列表和应用补丁列表。

      我们接着进入fetchPatchList方法

    1    protected List<Patch> fetchPatchList() {
    2         return patchManipulate.fetchPatchList(context);
    3     }

      他返回了patchManipulate的fetchPatchList方法,这个对象便是我们在初始化的时候传进来的。我们进入看一看

     1     @Override
     2     protected List<Patch> fetchPatchList(Context context) {
     3         Patch patch = new Patch();
     4         patch.setName("test patch");
     5         patch.setLocalPath(Environment.getExternalStorageDirectory().getPath()+
     6                 File.separator+"robust"+File.separator+"patch");
     7         patch.setPatchesInfoImplClassFullName("com.example.tyr.testrobust.PatchesInfoImpl");
     8         List<Patch> patches = new ArrayList<>();
     9         patches.add(patch);
    10         return patches;
    11     }

      我们将这个PatchesInfoImpl拉进到列表中,那么这个PatchInfoImpl是在哪里那?我们后面再说。

      接着看applyPatchList方法

      

    protected void applyPatchList(List<Patch> patches) {
            if (null == patches || patches.isEmpty()) {
                return;
            }
            Log.d("robust", " patchManipulate list size is " + patches.size());
            for (Patch p : patches) {
                if (p.isAppliedSuccess()) {
                    Log.d("robust", "p.isAppliedSuccess() skip " + p.getLocalPath());
                    continue;
                }
                if (patchManipulate.ensurePatchExist(p)) {
                    boolean currentPatchResult = false;
                    try {
                        currentPatchResult = patch(context, p);
                    } catch (Throwable t) {
                        robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
                    }
                    if (currentPatchResult) {
                        //设置patch 状态为成功
                        p.setAppliedSuccess(true);
                        //统计PATCH成功率 PATCH成功
                        robustCallBack.onPatchApplied(true, p);
    
                    } else {
                        //统计PATCH成功率 PATCH失败
                        robustCallBack.onPatchApplied(false, p);
                    }
    
                    Log.d("robust", "patch LocalPath:" + p.getLocalPath() + ",apply result " + currentPatchResult);
    
                }
            }
        }

      可以看到for循环patches中的每一个patch并调用patch方法。我们接着进入patch方法。

     1 protected boolean patch(Context context, Patch patch) {
     2         if (!patchManipulate.verifyPatch(context, patch)) {
     3             robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
     4             return false;
     5         }
     6 
     7         DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
     8                 null, PatchExecutor.class.getClassLoader());
     9         patch.delete(patch.getTempPath());
    10 
    11         Class patchClass, oldClass;
    12 
    13         Class patchsInfoClass;
    14         PatchesInfo patchesInfo = null;
    15         try {
    16             Log.d("robust", "PatchsInfoImpl name:" + patch.getPatchesInfoImplClassFullName());
    17             patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
    18             patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
    19             Log.d("robust", "PatchsInfoImpl ok");
    20         } catch (Throwable t) {
    21             robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:108");
    22             Log.e("robust", "PatchsInfoImpl failed,cause of" + t.toString());
    23             t.printStackTrace();
    24         }
    25 
    26         if (patchesInfo == null) {
    27             robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
    28             return false;
    29         }
    30 
    31         //classes need to patch
    32         List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
    33         if (null == patchedClasses || patchedClasses.isEmpty()) {
    34             robustCallBack.logNotify("patchedClasses is null or empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:122");
    35             return false;
    36         }
    37 
    38         for (PatchedClassInfo patchedClassInfo : patchedClasses) {
    39             String patchedClassName = patchedClassInfo.patchedClassName;
    40             String patchClassName = patchedClassInfo.patchClassName;
    41             if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
    42                 robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
    43                 continue;
    44             }
    45             Log.d("robust", "current path:" + patchedClassName);
    46             try {
    47                 oldClass = classLoader.loadClass(patchedClassName.trim());
    48                 Field[] fields = oldClass.getDeclaredFields();
    49                 Log.d("robust", "oldClass :" + oldClass + "     fields " + fields.length);
    50                 Field changeQuickRedirectField = null;
    51                 for (Field field : fields) {
    52                     if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
    53                         changeQuickRedirectField = field;
    54                         break;
    55                     }
    56                 }
    57                 if (changeQuickRedirectField == null) {
    58                     robustCallBack.logNotify("changeQuickRedirectField  is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
    59                     Log.d("robust", "current path:" + patchedClassName + " something wrong !! can  not find:ChangeQuickRedirect in" + patchClassName);
    60                     continue;
    61                 }
    62                 Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);
    63                 try {
    64                     patchClass = classLoader.loadClass(patchClassName);
    65                     Object patchObject = patchClass.newInstance();
    66                     changeQuickRedirectField.setAccessible(true);
    67                     changeQuickRedirectField.set(null, patchObject);
    68                     Log.d("robust", "changeQuickRedirectField set sucess " + patchClassName);
    69                 } catch (Throwable t) {
    70                     Log.e("robust", "patch failed! ");
    71                     t.printStackTrace();
    72                     robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
    73                 }
    74             } catch (Throwable t) {
    75                 Log.e("robust", "patch failed! ");
    76                 t.printStackTrace();
    77                 robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");
    78             }
    79         }
    80         Log.d("robust", "patch finished ");
    81         return true;
    82     }

      可以看到我们的类加载器在这里加载了我们的patch,我们接下来可以看到classloader加载了我们的PatchInfoImpl类。在这个类中继承了Robust的PatchInfo接口,这里只有一个方法

    1 public interface PatchesInfo {
    2     List<PatchedClassInfo> getPatchedClassesInfo();
    3 }  

      他拉取了我们需要修改的类的信息。

      这里面的PatchedClassInfo中保存了两个类的信息,一个是我们需要修改的类PatchedClass和修改他的类PatchClass。

      第39,40行Robust拿到了这两个类的名字。

      第48行通过反射获取了我们需要修改的类的所有field

      接下来是一个for循环获取到我们注入代码中的静态ChangeQuickRedirect对象。

      获取到对象后我们看第64行他加载了我们PatchClass的类

      接下来的65,66,67三行,我们可以看到他通过反射将我们PatchedClass即oldClass中的changeQuickRedirect字段赋值为我们的PatchClass。至于这个PatchClass是什么。我们接下来说。

      到目前为止,我们可以看到,插桩后的逻辑已经说完了,不得不说Robust的原理还是比较通俗易懂的。我们接下来回答前面的两个剩余问题,PatchInfoImpl和PatchClass在哪里。我们顺着我们的Patch.jar去寻找。反编译后得到如下列表。

      

      找到了我们的PatchesInfoImpl,而我们的PatchClass就是RobustActivityPatchControl了

      我们先来看一看PatchesInfoImpl做了什么

     1 import com.meituan.robust.PatchedClassInfo;
     2 import com.meituan.robust.PatchesInfo;
     3 import java.util.ArrayList;
     4 import java.util.List;
     5 
     6 public class PatchesInfoImpl
     7   implements PatchesInfo
     8 {
     9   public List getPatchedClassesInfo()
    10   {
    11     ArrayList localArrayList = new ArrayList();
    12     localArrayList.add(new PatchedClassInfo("com.example.tyr.testrobust.RobustActivity", "com.example.tyr.testrobust.RobustActivityPatchControl"));
    13     com.meituan.robust.utils.EnhancedRobustUtils.isThrowable = false;
    14     return localArrayList;
    15   }
    16 }

      可以看到他把我们的patchedClass和patchClass加入了list中,也就是上面返回的信息。

      我们接着看我们注入的这个patchClass中的方法

     1 public class RobustActivityPatchControl
     2   implements ChangeQuickRedirect
     3 {
     4   public static final String MATCH_ALL_PARAMETER = "(\w*\.)*\w*";
     5   private static final Map<Object, Object> keyToValueRelation = new WeakHashMap();
     6 
     7   private static Object fixObj(Object paramObject)
     8   {
     9     Object localObject = paramObject;
    10     if ((paramObject instanceof Byte))
    11       if (((Byte)paramObject).byteValue() == 0)
    12         break label32;
    13     label32: for (boolean bool = true; ; bool = false)
    14     {
    15       localObject = new Boolean(bool);
    16       return localObject;
    17     }
    18   }
    19 
    20   public Object accessDispatch(String paramString, Object[] paramArrayOfObject)
    21   {
    22     label134: 
    23     while (true)
    24       try
    25       {
    26         if (!paramString.split(":")[2].equals("false"))
    27           continue;
    28         if (keyToValueRelation.get(paramArrayOfObject[(paramArrayOfObject.length - 1)]) != null)
    29           continue;
    30         RobustActivityPatch localRobustActivityPatch = new RobustActivityPatch(paramArrayOfObject[(paramArrayOfObject.length - 1)]);
    31         keyToValueRelation.put(paramArrayOfObject[(paramArrayOfObject.length - 1)], null);
    32         break label134;
    33         if (!"12".equals(paramString.split(":")[3]))
    34           break;
    35         localRobustActivityPatch.onCreate((Bundle)paramArrayOfObject[0]);
    36         return null;
    37         localRobustActivityPatch = (RobustActivityPatch)keyToValueRelation.get(paramArrayOfObject[(paramArrayOfObject.length - 1)]);
    38         break label134;
    39         localRobustActivityPatch = new RobustActivityPatch(null);
    40         continue;
    41       }
    42       catch (Throwable paramString)
    43       {
    44         paramString.printStackTrace();
    45         return null;
    46       }
    47     return null;
    48   }
    49 
    50   public Object getRealParameter(Object paramObject)
    51   {
    52     Object localObject = paramObject;
    53     if ((paramObject instanceof RobustActivity))
    54       localObject = new RobustActivityPatch(paramObject);
    55     return localObject;
    56   }
    57 
    58   public boolean isSupport(String paramString, Object[] paramArrayOfObject)
    59   {
    60     paramString = paramString.split(":")[3];
    61     return ":12:".contains(":" + paramString + ":");
    62   }
    63 }

      我们可以看到它实现了Robust的ChangeQuickRedirect接口,并实现了他们的两个方法accessDispatch和isSupport的两个方法,也就是PatchProxy中调用的这两个方法。

      先说isSupport方法

      这里的isSupport方法是混淆后的,我们可以看到在PatchProxy类中,他传入了classMethod的名字和这个方法所需要的参数。校验后进行判断。

      在PatchProxy中他传入了的classMethod格式为className:methodName:isStatic:methodNumber。这里只校验了方法的number。这里是在accessDispatch中传入,目测插桩后的代码有所改动。

      继续说accessPatch方法。

      第26行校验了是否为静态方法。将参数数组传给了RobustActivityPatch这个类,并调用了它的onCreat方法,莫名的熟悉感,这个就是我们标注为Modify标签的那个类。

      我们接下来看一看RobustActivityPatch这个类

      

     1 import android.os.Bundle;
     2 import android.support.v7.app.c;
     3 import android.view.View;
     4 import android.widget.TextView;
     5 import com.meituan.robust.utils.EnhancedRobustUtils;
     6 
     7 public class RobustActivityPatch
     8 {
     9   RobustActivity originClass;
    10 
    11   public RobustActivityPatch(Object paramObject)
    12   {
    13     this.originClass = ((RobustActivity)paramObject);
    14   }
    15 
    16   public static void staticRobustonCreate(RobustActivityPatch paramRobustActivityPatch, RobustActivity paramRobustActivity, Bundle paramBundle)
    17   {
    18     RobustActivityPatchRobustAssist.staticRobustonCreate(paramRobustActivityPatch, paramRobustActivity, paramBundle);
    19   }
    20 
    21   public Object[] getRealParameter(Object[] paramArrayOfObject)
    22   {
    23     if ((paramArrayOfObject == null) || (paramArrayOfObject.length < 1))
    24       return paramArrayOfObject;
    25     Object[] arrayOfObject = new Object[paramArrayOfObject.length];
    26     int i = 0;
    27     if (i < paramArrayOfObject.length)
    28     {
    29       if ((paramArrayOfObject[i] instanceof Object[]))
    30         arrayOfObject[i] = getRealParameter((Object[])paramArrayOfObject[i]);
    31       while (true)
    32       {
    33         i += 1;
    34         break;
    35         if (paramArrayOfObject[i] == this)
    36         {
    37           arrayOfObject[i] = this.originClass;
    38           continue;
    39         }
    40         arrayOfObject[i] = paramArrayOfObject[i];
    41       }
    42     }
    43     return arrayOfObject;
    44   }
    45 
    46   protected void onCreate(Bundle paramBundle)
    47   {
    48     staticRobustonCreate(this, this.originClass, paramBundle);
    49     EnhancedRobustUtils.invokeReflectMethod("setContentView", ((RobustActivityPatch)this).originClass, getRealParameter(new Object[] { new Integer(2131296284) }), new Class[] { Integer.TYPE }, c.class);
    50     paramBundle = (View)EnhancedRobustUtils.invokeReflectMethod("findViewById", ((RobustActivityPatch)this).originClass, getRealParameter(new Object[] { new Integer(2131165307) }), new Class[] { Integer.TYPE }, c.class);
    51     if (paramBundle == this);
    52     for (paramBundle = ((RobustActivityPatch)paramBundle).originClass; ; paramBundle = (TextView)paramBundle)
    53     {
    54       String str = (String)EnhancedRobustUtils.invokeReflectMethod("RobustPublicgetString", new RobustActivityInLinePatch(getRealParameter(new Object[] { this })[0]), getRealParameter(new Object[0]), null, null);
    55       Object localObject = paramBundle;
    56       if (paramBundle == this)
    57         localObject = ((RobustActivityPatch)paramBundle).originClass;
    58       EnhancedRobustUtils.invokeReflectMethod("setText", localObject, getRealParameter(new Object[] { str }), new Class[] { CharSequence.class }, TextView.class);
    59       return;
    60     }
    61   }
    62 }

      看到onCreate方法,第48行,这里我们可以看到他特殊处理了一下我们在RobustActivity的onCreate方法,感觉有点怪怪的,这里似乎是又执行了一边RobustActivity的OnCreate方法,而不是super.onCreate。

      ps:看了看美团官方关于super的解析,似乎是这样的,他通过调用RobustActivity的OnCreate,将class文件中的invokevirtual指令替换为invokesuper指令,从而达到super的效果,这里面还有个问题,如果这样调用会出现这样的问题

    Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)

      Robust的解决方案是使这个类也继承RobustActivity的父类,我们可以看到RobustActivityPatchRobustAssist类果然继承了一个类,但是由于混淆我们看到的是他继承了一个c的类,猜测它应该就是RobustActivity的父类AppCompatActivity。

      验证一下打印dex文件

      

      看到invoke-super指令,现在可以确定了

      再看一下RobustActivityPatchRobustAssist的父类,这绝对就是android.support.v7.app.AppcompatActivity了。

      

      然后我们可以看到他执行了setContentView,findViewById这里传入的两串数字便是我们的布局和空间在R类的数字。

      然后我们可以看到它执行到了我们修改代码的地方。

      第54行它调用了RobustPublicgetString方法,又是莫名的熟悉感,,

      进入RobustActivityInLinePatch看一看。

     1 public class RobustActivityInLinePatch
     2 {
     3   RobustActivity originClass;
     4 
     5   public RobustActivityInLinePatch(Object paramObject)
     6   {
     7     this.originClass = ((RobustActivity)paramObject);
     8   }
     9 
    10   private String getString()
    11   {
    12     return "hello robust";
    13   }
    14 
    15   public String RobustPublicgetString()
    16   {
    17     return getString();
    18   }
    19 
    20   public Object[] getRealParameter(Object[] paramArrayOfObject)
    21   {
    22     if ((paramArrayOfObject == null) || (paramArrayOfObject.length < 1))
    23       return paramArrayOfObject;
    24     Object[] arrayOfObject = new Object[paramArrayOfObject.length];
    25     int i = 0;
    26     if (i < paramArrayOfObject.length)
    27     {
    28       if ((paramArrayOfObject[i] instanceof Object[]))
    29         arrayOfObject[i] = getRealParameter((Object[])paramArrayOfObject[i]);
    30       while (true)
    31       {
    32         i += 1;
    33         break;
    34         if (paramArrayOfObject[i] == this)
    35         {
    36           arrayOfObject[i] = this.originClass;
    37           continue;
    38         }
    39         arrayOfObject[i] = paramArrayOfObject[i];
    40       }
    41     }
    42     return arrayOfObject;
    43   }
    44 }

      可以看到我们传入的getString方法出现在了这里。

    二、总结

      到目前为止,Robust的逻辑算是走通了。

      目前为止,我认为Robust的核心应该算是它自动插桩的那一部分,目前暂时不涉及了,下一篇将会了解一下热修复背后的动态加载。

    参考资料:

      Android中热修复框架Robust原理解析+并将框架代码从"闭源"变成"开源"(上篇)

      Android热更新方案Robust

  • 相关阅读:
    文件上传之 MultipartFile
    EL表达式从数组 Map List集合取值
    Servlet监听器
    分页技术框架(Pager-taglib)学习三(pager-taglib中传递参数时中文乱码问题)
    分页技术框架(Pager-taglib)学习二(SSH数据库分页)
    分页技术框架(Pager-taglib)学习一(页面分页)
    从request获取各种路径总结
    jsp的页面包含——静态包含、动态包含
    Java对象的浅克隆和深克隆
    Java序列化与反序列化学习(三):序列化机制与原理
  • 原文地址:https://www.cnblogs.com/yrstudy/p/8977315.html
Copyright © 2011-2022 走看看