发表于

2022-06-10 速石面试 记录

Authors

1. Java cglib代理

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

1) 静态代理

这种代理方式需要代理对象和目标对象实现一样的接口。

  • 优点:可以在不修改目标对象的前提下扩展目标对象的功能。
  • 缺点:
    • 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
    • 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
interface Subject {
    void request();
}

class RealSubject implements Subject {
    public void request(){
        System.out.println("RealSubject");
    }
}

class Proxy implements Subject {
    private Subject subject;

    public Proxy(Subject subject){
        this.subject = subject;
    }
    public void request(){
        System.out.println("begin");
        subject.request();
        System.out.println("end");
    }
}

public class ProxyTest {
    public static void main(String args[]) {
        RealSubject subject = new RealSubject();
        Proxy p = new Proxy(subject);
        p.request();
    }
}

2) 动态代理

动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。

静态代理与动态代理的区别主要在:

  • 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
  • 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中

特点:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。

public interface MyInterface {
    void doSth();    
    void doSthElse(String args);    
}

public class RealObject implements MyInterface {
    @Override
    public void doSth() {
        System.out.println("do something");
    }

    @Override
    public void doSthElse(String args) {
        System.out.println("do sth else "+ args);
    }
}

public class MyInvocatioHandler implements InvocationHandler {

    private Object target; //添加被代理类引用

    public MyInvocatioHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("-----before-----");
       Object result = method.invoke(target, args);
       System.out.println("-----end-----");

       return result;
    }

    public Object getProxy() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        //类可以实现多个接口,因此这里的接口是个数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
        //this即MyInvocatioHandler实例,其包含被代理类的引用,以及重写的方法,newProxyInstance方法将利用这些参数创建一个代理类的实例
        return Proxy.newProxyInstance(loader, interfaces, this);
    }

}

public class JDKProxyTest {

    public static void main(String[] args) {
        // 1. 创建被代理实例
        RealObject realObject = new RealObject();
        // 2. 创建自己实现的InvocatioHandler实例
        MyInvocatioHandler handler = new MyInvocatioHandler(realObject);
        // 3. 创建代理实例
        MyInterface myInterface = (MyInterface) handler.getProxy();
        // 4. 通过代理调用方法
        myInterface.doSth();
    }

}

3) CGlib代理

cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

cglib特点

  • JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。 如果想代理没有实现接口的类,就可以使用CGLIB实现。
  • CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。 它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。
  • CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。 不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。

cglib与动态代理最大的区别:使用动态代理的对象必须实现一个或多个接口,使用cglib代理的对象则无需实现接口,达到代理类无侵入。

public final class Train {
    public void move(){  
        System.out.println("train running…");  
    }      
}

public class CGLibProxy implements MethodInterceptor {

    private Object proxied;

    public CGLibProxy(Object proxied) {
        this.proxied = proxied;
    }

    public Object getProxy(){
        //cglib 中增强器,用来创建动态代理
        Enhancer enhancer = new Enhancer();
        //设置要创建动态代理的类
        enhancer.setSuperclass(proxied.getClass());
        //设置回调,这里相当于是对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦截  
        enhancer.setCallback(this);
        //创建代理类
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("cglib proxy start...");
        proxy.invokeSuper(obj, args);
        System.out.println("cglib proxy end...");
        return null;
    }

}

public static void main(String[] args) {
    CGLibProxy proxy = new CGLibProxy(new Train());
    Train t = (Train)proxy.getProxy();
    t.move();
}

2. React: setState 异步同步, 同步场景

所谓同步还是异步指的是调用 setState 之后是否马上能得到最新的 state

不仅仅是setState了, 在对 function 类型组件中的 hook 进行操作时也是一样, 最终决定setState是同步渲染还是异步渲染的关键因素是ReactFiberWorkLoop工作空间的执行上下文.

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
  const priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        flushSyncCallbackQueue();
      }
    }
  } else {
    // Schedule a discrete update but only if it's not Sync.
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      // Only updates at user-blocking priority or greater are considered
      // discrete, even inside a discrete event.
      (priorityLevel === UserBlockingPriority ||
        priorityLevel === ImmediatePriority)
    ) {
      // This is the result of a discrete event. Track the lowest priority
      // discrete update per root so we can flush them early, if needed.
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
      } else {
        const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
        if (
          lastDiscreteTime === undefined ||
          lastDiscreteTime > expirationTime
        ) {
          rootsWithPendingDiscreteUpdates.set(root, expirationTime);
        }
      }
    }
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
}

可以看到, 是否同步渲染调度决定代码是flushSyncCallbackQueue(). 进入该分支的条件:

  1. 必须是legacy模式, concurrent模式下expirationTime不会为Sync
  2. executionContext === NoContext, 执行上下文必须要为空.

结论

  • 同步:
    1. 首先在legacy模式下
    2. 在执行上下文为空的时候去调用setState
      • 可以使用异步调用如setTimeout, Promise, MessageChannel等
      • 可以监听原生事件, 注意不是合成事件, 在原生事件的回调函数中执行 setState 就是同步的
  • 异步:
    1. 如果是合成事件中的回调, executionContext |= DiscreteEventContext, 所以不会进入, 最终表现出异步
    2. concurrent 模式下都为异步

Link 组件最终会渲染为 HTML 标签 <a>,它的 to、query、hash 属性会被组合在一起并渲染为 href 属性。虽然 Link 被渲染为超链接,但在内部实现上使用脚本拦截了浏览器的默认行为,然后调用了history.pushState 方法。

Link 只负责触发 url 变更,Route 只负责根据 url 渲染组件

相比于 <a> 标签,<Link> 避免了不必要的渲染

其实就是<a>会触发全页面渲染,<Link>不会, 然后<Link>接管了<a>click方法

<Link>hash 模式的时候是通过锚点来切换 url 路径实现的所以并不会触发实际的物理跳页,在 history 模式时是通过阻止默认行为来实现的拦截点击事件利用 pushstate 来切换 url 路径。

4. Redux:reducer 为什么是纯函数

纯函数

  • 相同的输入永远返回相同的输出
  • 不修改函数的输入值
  • 不依赖外部环境状态
  • 无任何副作用

为什么一定是纯函数

首先reducer的作用是 接收旧的 state 和 action,返回新的 state

他就是起一个对数据做简单处理后返回state的作用,因为reduxreducer设计成只负责这个作用。

reducer的职责不允许有副作用,副作用简单来说就是不确定性,如果reducer有副作用,那么返回的state就不确定

reducer需要是保证新旧 state 不是同一个对象引用, 所以不能直接修改旧 state 中的数据, 然后返回旧 state, 这会导致 combineReducers 中的判断失效( hasChanged = hasChanged || nextStateForKey !== previousStateForKey ), 最后redux会直接返回旧 state, 从而导致操作无效

5. Devops: Dockerfile & build 优化

  • 避免安装没用的依赖包
  • 编写.dockerignore 文件
  • apt-get install时 一并安装
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*
  • 拆分服务到最小粒度
  • 尽可能的减少层数: COPY RUN ADD 命令会创建 image layer , 尽可能只将打包之后的最终文件 诸如 jar或者是前端的dist文件夹拷贝到 最终推到registry 的镜像中,尽可能使用alpine 镜像
  • 尽可能利用缓存:建议打包的时候用一个 底包 像是包含了build-deps 的底包镜像