Sunday, January 17, 2016

Java Proxy



http://www.cnblogs.com/hujunzheng/p/5134478.html
  代理模式是常用的java设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。 

AVA各种动态代理实现的比较

接口

interface AddInterface{
    int add(int a, int b);
}

interface SubInterface{
    int sub(int a, int b);
}

实现类

class Arithmetic implements AddInterface, SubInterface{
    @Override
    public int sub(int a, int b) {
        return a-b;
    }

    @Override
    public int add(int a, int b) {
        return a+b;
    }
}

方式1: JDK自带的动态代理

实现方式

  Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。 

InvocationHandler接口的实现

class JdkDPQueryHandler implements InvocationHandler{
    private Arithmetic real;
    public JdkDPQueryHandler(Arithmetic real){
        this.real = real;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        Object result = method.invoke(real, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}

创建代理类并且调用代理类

public class Main{
    private static int a = 4, b = 2;
    
    public static Object createJDKProxy(Arithmetic real){
        Object proxyArithmetic = Proxy.newProxyInstance(real.getClass().getClassLoader(),
                real.getClass().getInterfaces(), new JdkDPQueryHandler(real)); 
        return proxyArithmetic;
    }
    
    public static void main(String[] args){
        Arithmetic real = new Arithmetic();
        Object proxyArithmetic = createJDKProxy(real);
        ((AddInterface)proxyArithmetic).add(a, b);
        ((SubInterface)proxyArithmetic).sub(a, b);
    }
}

方式2:动态字节码生成(cglib)

InvocationHandler接口的实现

class JdkDPQueryHandler implements InvocationHandler{
    private Arithmetic real;
    public JdkDPQueryHandler(Arithmetic real){
        this.real = real;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        Object result = method.invoke(real, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}

创建代理类并且调用代理类

public class Main{
    private static int a = 4, b = 2;
    
    public static Object createJDKProxy(Arithmetic real){
        Object proxyArithmetic = Proxy.newProxyInstance(real.getClass().getClassLoader(),
                real.getClass().getInterfaces(), new JdkDPQueryHandler(real)); 
        return proxyArithmetic;
    }
    
    public static void main(String[] args){
        Arithmetic real = new Arithmetic();
        Object proxyArithmetic = createJDKProxy(real);
        ((AddInterface)proxyArithmetic).add(a, b);
        ((SubInterface)proxyArithmetic).sub(a, b);
    }
}

方式2:动态字节码生成(cglib)

实现方式

  Enhancer和MethodInterceptor。Enhancer可以用来动态的生成一个类,这个类可以继承指定的一个类,实现指定的一些接口。同时,Enhancer在生成一个类之前需要指定一个Callback,当类方法调用时,方法的执行被分配给这个Callback,MethodInterceptor是一个使用比较多的继承自Callback的接口,它只有一个方法声明。

接口InvocationHandler(jdk中)和接口MethodInterceptor(cglib中)对比

public interface MethodInterceptor extends Callback  {  
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable;  
}  
public interface InvocationHandler {  
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;  
}  
    从参数构成上,methodInterceptor的输入参数比Invocationhandler多1个,其实前3个参数对象的含义与Invocationhandler的含义是相同的。
  第一个参数表示调用方法来自哪个对象;
  第二个参数表示调用方法的Method对象;
  第三个参数表示此次调用的输入参数列表;
  多出来的参数是MethodProxy 类型的,它应该是cglib生成用来代替Method对象的一个对象,使用MethodProxy比调用JDK自身的Method直接执行方法效率会有提升。

实现1

  MethodInterceptor接口的实现
class CglibDPQueryInterceptor implements MethodInterceptor{
    private Arithmetic real;
    public CglibDPQueryInterceptor(Arithmetic real){
        this.real = real;
    }
    
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        //Object result = method.invoke(real, args);//两种方式都是可以得
        Object result = proxy.invoke(real, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}
  创建代理类并调用代理类
public class Main{
    private static int a = 4, b = 2;
   public static Object createCglibProxy(Arithmetic real){
         Enhancer enhancer = new Enhancer();
         enhancer.setCallback(new CglibDPQueryInterceptor(real));
         enhancer.setInterfaces(real.getClass().getInterfaces());
         return enhancer.create();
    }
    
    public static void main(String[] args){
        Arithmetic real = new Arithmetic();      
        Object proxyArithmetic = createCglibProxy(real);        
        ((AddInterface)proxyArithmetic).add(a, b);
        ((SubInterface)proxyArithmetic).sub(a, b);
    }
}
   注意了,MethodProxy在对执行函数的时候,提供了2个方法
public Object invoke (Object obj, Object[] args) throws Throwable  
public Object invokeSuper(Object obj, Object[] args) throws Throwable
  其中,javadoc上说这个invoke()方法可以用于相同类中的其他对象的方法执行,也就是说这个方法中的obj需要传入相同一个类的另一个对象(上述方法中就是传入了Arithmetic类的不同对象,否则会进入无限递归循环(测试之后还真是出现了StackOverflowError)。仔细的想一想就会发现,public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy)中的target是实现的代理类对象,通过target调用add()方法时会触发intercept()方法被调用,如果在intercept()方法中再调用method.invoke(target, args),就相当于add()方法中又调用add()方法,导致无限的递归循环。但是如果执行method.invoke(real, args)则不会,因为real和target是同一个类不同对象,real是真实逻辑主题,target是真实主题real的代理。
  下面一个例子来模拟一下:
interface SolveInterface{
    void solve();
}

class Real implements SolveInterface{
    public void solve(){
        System.out.println("Real Solve!");
    }
}

class Target extends Real{
    private Object obj;
    
    public void setObject(Object obj){
        this.obj = obj;
    }
    
    private void invoke(){
        try {
            Method method = SolveInterface.class.getMethod("solve", new Class[]{});
            method.invoke(obj, new Class[]{});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void solve(){
        System.out.println("Target Solve!");
        invoke();
    }
}
  
public class Main{public static void main(String[] args) throws Exception{     
        Target target = new Target();
        target.setObject(new Real());//正确
        //target.setObject(target);//发生循环调用
        target.solve();
    }
}
  其实Method的invoke()方法会根据obj的类型去调用对应的solve()方法,也就是多态性。

实现2

  MethodInterceptor接口的实现
class CglibDPQueryInterceptor implements MethodInterceptor{
    
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        //  打印类信息 :target.getClass();省略
        Object result = proxy.invokeSuper(target, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}
  创建代理类并调用代理类
public class Main{
    private static int a = 4, b = 2;
public static Object createCglibProxy(){
         Enhancer enhancer = new Enhancer();
         enhancer.setCallback(new CglibDPQueryInterceptor());
         enhancer.setSuperclass(Arithmetic.class);
         return enhancer.create();
    }
    
    public static void main(String[] args){
        Arithmetic real = new Arithmetic();

        Object proxyArithmetic = createCglibProxy();
        
        ((AddInterface)proxyArithmetic).add(a, b);
        ((SubInterface)proxyArithmetic).sub(a, b);
    }
}
  注意了,实现2中Enhancer 没有设置接口,因为设置了Superclass了(也就是代理类的父类是Arithmetic),我们的代理类会继承它,而Arithmetic已经实现了我们的接口。为了证明这一点,可以在MethodInterceptor的 intercept方法中打印 target.getClass()的类信息,你会发现cglib的两种方式代理类的父类是不同的。如下:
  (如果需要打印类信息代码,请参考:http://www.cnblogs.com/hujunzheng/p/5132943.html
  实现1:
public class com.test.Arithmetic$$EnhancerByCGLIB$$4fa786eb extends java.lang.Object
  实现2:
public class com.test.Arithmetic$$EnhancerByCGLIB$$4fa786eb extends com.test.Arithmetic

方式3:javassist生成动态代理(代理工厂创建 或者 动态代码创建)  

  Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图: 

实现1:

接口的实现
class JavassistDPQueryHandler implements MethodHandler{

    @Override
    public Object invoke(Object target, Method method, Method proxy, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        Object result = proxy.invoke(target, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}
创建代理类并调用代理类
public class Main{
    private static int a = 4, b = 2;
public static Object createJavassistProxy() throws Exception{
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(Arithmetic.class);
        factory.setHandler(new JavassistDPQueryHandler());
        return factory.createClass().newInstance();
    }
    
    public static void main(String[] args) throws Exception{
        Arithmetic real = new Arithmetic();
        
        Object proxyArithmetic = createJavassistProxy();
        
        ((AddInterface)proxyArithmetic).add(a, b);
        ((SubInterface)proxyArithmetic).sub(a, b);
    }
}
  注意:MethodHandler接口中invoke方法的定义,如下:
public Object invoke(Object target, Method method, Method proxy, Object[] args)
  method代表调用方法的Method对象,proxy是代理类产生并代替method的对象,否则用method.invoke(target, args)会产生无限循环调用。

实现2:

  (来自:http://cuishen.iteye.com/blog/421464),代码注释很详细,仔细研究一下就会懂了!
  javassist使用动态java代码常见代理过程和前文的方法略有不同。javassist内部可以通过动态java代码,生成字节码。这种方式创建的动态代理可以非常灵活,甚至可以在运行时产生业务逻辑。
//自定义拦截器接口
interface InterceptorHandler {  
    
    /** 
     * 调用动态代理对象的方法将反射本方法,可在本方法实现中添加类似AOP的事前事后操作,只有在本方法体中加入如下代码 
     * 被代理的方法才会被执行,返回值将返回给代理最后返回给程序 
     * @param obj Object 被代理的对象 
     * @param method Method 被代理对象的方法 
     * @param args Object[] 被代理对象的方法的参数 
     * @return Object 被代理对象的方法执行后的返回值 
     * @throws Throwable 
     */  
    public Object invoke(Object obj, Method method, Object[] args) throws Throwable;  
}  

//拦截器的实现
class InterceptorHandlerImpl implements InterceptorHandler{
    @Override
    public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.println(method);
        System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));
        Object result = method.invoke(obj, args);
        System.out.println("the method: "+methodName+"结束, 结果: " + result);
        return result;
    }
}


class MyProxyImpl {  
    /** 动态代理类的类名后缀 */  
    private final static String PROXY_CLASS_NAME_SUFFIX = "$MyProxy_";  
    /** 拦截器接口 */  
    private final static String INTERCEPTOR_HANDLER_INTERFACE = "com.test.InterceptorHandler";  
    /** 动态代理类的类名索引,防止类名重复 */  
    private static int proxyClassIndex = 1;  
      
    /** 
     * 暴露给用户的动态代理接口,返回某个接口的动态代理对象,注意本代理实现需和com.cuishen.myAop.InterceptorHandler拦截器配合 
     * 使用,即用户要使用本动态代理,需先实现com.cuishen.myAop.InterceptorHandler拦截器接口 
     * @param interfaceClassName String 要动态代理的接口类名, e.g test.StudentInfoService 
     * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl 
     * @param interceptorHandlerImplClassName String 用户提供的拦截器接口的实现类的类名 
     * @return Object 返回某个接口的动态代理对象 
     * @throws InstantiationException 
     * @throws IllegalAccessException 
     * @throws NotFoundException 
     * @throws CannotCompileException 
     * @throws ClassNotFoundException 
     * @see com.cuishen.myAop.InterceptorHandler 
     */  
    public static Object newProxyInstance(String interfaceClassName, String classToProxy, String interceptorHandlerImplClassName) throws InstantiationException, IllegalAccessException, NotFoundException, CannotCompileException, ClassNotFoundException {  
        Class interfaceClass = Class.forName(interfaceClassName);  
        Class interceptorHandlerImplClass = Class.forName(interceptorHandlerImplClassName);  
        return dynamicImplementsInterface(classToProxy, interfaceClass, interceptorHandlerImplClass);  
    }  
      
    /** 
     * 动态实现要代理的接口 
     * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl 
     * @param interfaceClass Class 要动态代理的接口类, e.g test.StudentInfoService 
     * @param interceptorHandlerImplClass Class 用户提供的拦截器接口的实现类 
     * @return Object 返回某个接口的动态代理对象 
     * @throws NotFoundException 
     * @throws CannotCompileException 
     * @throws InstantiationException 
     * @throws IllegalAccessException 
     */  
    private static Object dynamicImplementsInterface(String classToProxy, Class interfaceClass, Class interceptorHandlerImplClass) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {  
        ClassPool cp = ClassPool.getDefault();  
        String interfaceName = interfaceClass.getName();  
        //动态指定代理类的类名  
        String proxyClassName = interfaceName + PROXY_CLASS_NAME_SUFFIX + proxyClassIndex++;  
        //要实现的接口的包名+接口名  
        String interfaceNamePath = interfaceName;  
          
        CtClass ctInterface = cp.getCtClass(interfaceNamePath);  
        CtClass cc = cp.makeClass(proxyClassName);  
        cc.addInterface(ctInterface);  
        Method [] methods = interfaceClass.getMethods();  
        for(int i = 0; i < methods.length; i++) {  
            Method method = methods[i];  
            dynamicImplementsMethodsFromInterface(classToProxy, cc, method, interceptorHandlerImplClass, i);  
        }  
        return (Object)cc.toClass().newInstance();  
    }  
      
    /** 
     * 动态实现接口里的方法 
     * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl 
     * @param implementer CtClass 动态代理类的包装 
     * @param methodToImpl Method 动态代理类里面要实现的接口方法的包装 
     * @param interceptorClass Class 用户提供的拦截器实现类 
     * @param methodIndex int 要实现的方法的索引 
     * @throws CannotCompileException 
     */  
    private static void dynamicImplementsMethodsFromInterface(String classToProxy, CtClass implementer, Method methodToImpl, Class interceptorClass, int methodIndex) throws CannotCompileException {  
        String methodCode = generateMethodCode(classToProxy, methodToImpl, interceptorClass, methodIndex);  
        CtMethod cm = CtNewMethod.make(methodCode, implementer);  
        implementer.addMethod(cm);  
    }  
      
    /** 
     * 动态组装方法体,当然代理里面的方法实现并不是简单的方法拷贝,而是反射调用了拦截器里的invoke方法,并将接收到的参数进行传递 
     * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl 
     * @param methodToImpl Method 动态代理类里面要实现的接口方法的包装 
     * @param interceptorClass Class 用户提供的拦截器实现类 
     * @param methodIndex int 要实现的方法的索引 
     * @return String 动态组装的方法的字符串 
     */  
    private static String generateMethodCode(String classToProxy, Method methodToImpl, Class interceptorClass, int methodIndex) {  
        String methodName = methodToImpl.getName();  
        String methodReturnType = methodToImpl.getReturnType().getName();  
        Class[] parameters = methodToImpl.getParameterTypes();  
        Class[] exceptionTypes = methodToImpl.getExceptionTypes();  
        StringBuffer exceptionBuffer = new StringBuffer();  
        //组装方法的Exception声明  
        if(exceptionTypes.length > 0) exceptionBuffer.append(" throws ");  
        for(int i = 0; i < exceptionTypes.length; i++) {  
            if(i != exceptionTypes.length - 1) exceptionBuffer.append(exceptionTypes[i].getName()).append(",");  
            else exceptionBuffer.append(exceptionTypes[i].getName());  
        }  
        StringBuffer parameterBuffer = new StringBuffer();  
        //组装方法的参数列表  
        for(int i = 0; i < parameters.length; i++) {  
            Class parameter = parameters[i];  
            String parameterType = parameter.getName();  
            //动态指定方法参数的变量名  
            String refName = "a" + i;  
            if(i != parameters.length - 1) parameterBuffer.append(parameterType).append(" " + refName).append(",");  
            else parameterBuffer.append(parameterType).append(" " + refName);  
        }  
        StringBuffer methodDeclare = new StringBuffer();  
        //方法声明,由于是实现接口的方法,所以是public  
        methodDeclare.append("public ").append(methodReturnType).append(" ").append(methodName).append("(").append(parameterBuffer).append(")").append(exceptionBuffer).append(" {\n");  
        String interceptorImplName = interceptorClass.getName();  
        //方法体  
        methodDeclare.append(INTERCEPTOR_HANDLER_INTERFACE).append(" interceptor = new ").append(interceptorImplName).append("();\n");  
        //反射调用用户的拦截器接口  
        methodDeclare.append("Object returnObj = interceptor.invoke(Class.forName(\"" + classToProxy + "\").newInstance(), Class.forName(\"" + classToProxy + "\").getMethods()[" + methodIndex + "], ");  
        //传递方法里的参数  
        if(parameters.length > 0) methodDeclare.append("new Object[]{");   
        for(int i = 0; i < parameters.length; i++) {  
            //($w) converts from a primitive type to the corresponding wrapper type: e.g.  
            //Integer i = ($w)5;  
            if(i != parameters.length - 1) methodDeclare.append("($w)a" + i + ",");  
            else methodDeclare.append("($w)a" + i);  
        }  
        if(parameters.length > 0) methodDeclare.append("});\n");  
        else methodDeclare.append("null);\n");  
        //对调用拦截器的返回值进行包装  
        if(methodToImpl.getReturnType().isPrimitive()) {  
            if(methodToImpl.getReturnType().equals(Boolean.TYPE)) methodDeclare.append("return ((Boolean)returnObj).booleanValue();\n");  
            else if(methodToImpl.getReturnType().equals(Integer.TYPE)) methodDeclare.append("return ((Integer)returnObj).intValue();\n");  
            else if(methodToImpl.getReturnType().equals(Long.TYPE)) methodDeclare.append("return ((Long)returnObj).longValue();\n");  
            else if(methodToImpl.getReturnType().equals(Float.TYPE)) methodDeclare.append("return ((Float)returnObj).floatValue();\n");  
            else if(methodToImpl.getReturnType().equals(Double.TYPE)) methodDeclare.append("return ((Double)returnObj).doubleValue();\n");  
            else if(methodToImpl.getReturnType().equals(Character.TYPE)) methodDeclare.append("return ((Character)returnObj).charValue();\n");  
            else if(methodToImpl.getReturnType().equals(Byte.TYPE)) methodDeclare.append("return ((Byte)returnObj).byteValue();\n");  
            else if(methodToImpl.getReturnType().equals(Short.TYPE)) methodDeclare.append("return ((Short)returnObj).shortValue();\n");  
        } else {  
            methodDeclare.append("return (" + methodReturnType + ")returnObj;\n");  
        }  
        methodDeclare.append("}");  
        System.out.println(methodDeclare.toString());  
        return methodDeclare.toString();  
    }  
      
}  

public class Main{     
    public static void main(String[] args) throws Exception{    
     //分别对应 代理类要实现的接口类名需要代理类的类名用户自定义拦截器实现类的类名
        Object proxyArithmetic = MyProxyImpl.newProxyInstance("com.test.ArithmeticInterface", "com.test.Arithmetic", 
                                    "com.test.InterceptorHandlerImpl");
        ((ArithmeticInterface)proxyArithmetic).add(a, b);
        ((ArithmeticInterface)proxyArithmetic).sub(a, b);        
    }
}
  打印一下动态实现接口的代码如下:
public int add(int a0,int a1) {
com.test.InterceptorHandler interceptor = new com.test.InterceptorHandlerImpl();
Object returnObj = interceptor.invoke(Class.forName("com.test.Arithmetic").newInstance(), Class.forName("com.test.Arithmetic").getMethods()[0], new Object[]{($w)a0,($w)a1});
return ((Integer)returnObj).intValue();
}
public int sub(int a0,int a1) {
com.test.InterceptorHandler interceptor = new com.test.InterceptorHandlerImpl();
Object returnObj = interceptor.invoke(Class.forName("com.test.Arithmetic").newInstance(), Class.forName("com.test.Arithmetic").getMethods()[1], new Object[]{($w)a0,($w)a1});
return ((Integer)returnObj).intValue();
}



Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts