首页 文章 精选 留言 我的

精选列表

搜索[面试],共4916篇文章
优秀的个人博客,低调大师

六个方向关于iOS100个面试题,你都会了吗?

常见问题 你昨天/这周学习了什么? 你为什么热衷于软件开发? 你对哪一种控制系统比较熟悉? 是否参与过GitHub项目? 是否参与过GitHub或其他同类型网站的iOS开源项目? 请描述一下你的iOS应用开发流程。 是否熟知CocoaPods?它是什么?如何运行的? 请概括一下你对软件授权的理解,及其对软件开发的影响。 请概括一下你在构建iOS应用时的测试过程。iOS应用如何实现对其他语言、日期格式以及货币单位的支持? 请描述一下Instruments及其作用。 关于iOS技术 请解释一下Handoff是什么,并简述它是如何实现iOS、Mac/网页应用互通的。 iCloud包含了哪些技术与服务? iOS扩展是指?能否列举一些热门或常见的范例? HealthKit是什么? HomeKit是什么? Apple Pay是什么?能否描述一下如何在应用中使用Apple Pay? 请解释一下iOS应用沙盒机制。 VoiceOver是什么?请举例解释一下iOS中的辅助功能(Accessibility)。开发者如何使用这些功能? iOS应用是如何实现后台多任务处理(Multitasking)的? Game Center针对iOS游戏有哪些功能? iBeacons是什么? Cocoa/Cocoa Touch是什么? 请概括一下Core Audio,Core Data以及Core Location各是什么。它们对iOS应用有何意义? 请描述SpriteKit和SceneKit的作用。 Metal是什么? 响应链(Responder Chain)是什么?它是如何发挥作用的? 按钮和其他控制方式对哪些操作做出回应? AppDelegate扮演着什么样的角色? 请解释一下NSUserDefaults。就你而言,你会如何在磁盘中对数组对象进行序列化? 你会如何储存用户的认证信息? 请问何为Keychain服务? 为什么移动设备上的缓存和压缩是不可或缺的? 请解释一下~/Documents,~/Library和~/tmp。 iOS中的~属于什么目录? AirPlay是如何运行的?换做是你,你会如何通过编程提高应用的实用性以及演示效果? 传感器,IO以及WiFi、拨号等连接方式如何在iOS平台上运作?它们有何利用价值?请扼要地谈谈你的观点。 iPad 2,iPad mini 1-3,iPad Retina,iPad Air 2,iPhone 5、5S、6以及6+在硬件性能方面有何差异?这对注重性能的应用有何限制? 关于编程 Cocoa Touch包含什么?不包含什么? 为什么Cocoa Touch的类名称是以两个大写字母开头的? Swift和Objective-C分别是什么?两者相比有何不同之处,又有何联系? 为什么Optional在Swift语言中非常重要? 请解释一下NSError。在Swift中,什么情况下能使用NSError ,什么情况下不能? 请说明如何使用Instancetype及其重要性。 在Swift中,什么时候该用let,什么时候该用var? 为什么map函数必不可少?该在什么情况下使用它? 你会选择什么工具来追踪Bug? 如果在Cocoa中发现一个Bug,你会如何处理? 如果应用的新版本出现了Regression的情况,该如何补救?如何防止用户在使用过程中遇到新的Bug? Objective-C的类是怎么执行的?Objective-C Runtime是如何实现的? iOS是如何提高安全性,保护用户隐私信息的? 应用可以下载并即刻显示数据。如何根据MVC来判断下载的最佳位置? MVC对代码库(Codebase)的设计有何影响? Controller Life-Cycle以及View Life-cycle分别有哪些调试方法? iOS使用的是哪些设计模式(Design Patterns)?你的代码库使用的是哪些设计模式? iOS提供哪些线程?如何充分利用这些线程? 请简要描述一下UIScrollView的执行过程。它是如何响应手势识别(Gesture Recognizer)、多点触控(Multi-Touch)和Run Loop的? 你认为iOS需要添加或改进哪些API? 关于界面 iPhone5、6、6+以及iPad Air 2的屏幕分辨率分别是多少? 分辨率的计算单位是什么? 请解释一下Interface Builder的作用以及NIB文件的概念。 iOS UI的图像储存类型是什么? 请描述一下Storyboard和标准NIB文件的差别。 设备状态栏(Device Status Bar)是什么?高度如何?是否透明?在手机通话或者导航状态下,它是如何显示的? 导航栏(Navigation Bar)是什么?能否拿出你的iPhone,指出你下载的哪些应用运用了导航栏? 选项卡(Tab Bar)和工具栏(Toolbar)分别是什么?两者之间有何共同点和不同点? 表视图(Table View)是什么?集合视图(Collection View)又是什么? 什么时候用“弹出(Popover)”属性最为合适? Split-view Controller是什么? 选取器视图(Picker View)适合存放哪类内容? 应该在什么情况下使用标签、文本域和文本视图? 分段控件(Segmented Control)的作用是什么? 模态视图(Modal View)是什么? iOS通知属于什么类型? 关于设计 iOS应用图标是指什么?请尽可能详细地描述一下。 最小尺寸和最大尺寸的应用图标分别是什么样子的? 应用图标能否包含透明的部分? Newsstand的图标与常规应用有何不同? 请解释一下启动画面(Launch Images)。 自动布局(Auto Layout)的作用是什么?请概括一下它是如何运行的。 设计软件时为什么要加上动画? 请描述一下软件设计中的交互和Feedback有什么作用。 设计iPhone和iPad应用时,应分别考虑哪些因素? 请描述一下原型设计对于软件开发的意义。其作用是什么? 关于App Store 应用内购买(In-App Purchases)是怎么回事?IAP能够为用户带来哪些新体验? 你是否在App Store上发布过应用?能否概括一下过程? iTunes Connect是什么? Provisioning Profiles是指? App ID是什么? iOS的开发和发布签名证书有何异同? 如何使用TestFlight?通过Ad-hoc发布应用的话,该如何使用UUID? 应何时验证购买收据? 发布iAds(苹果平台广告)有哪些要求? 趣味问答 最近有没有开发什么好玩的东西?你最引以为豪的作品是什么? 谈一谈你常用的开发工具都有哪些优势? 你最敬佩的独立Mac或者iOS应用开发者是谁? 最喜欢什么项目?哪种类型的? 你觉得Xcode有哪些需要改进的地方? iOS上你最喜欢哪些API? 是否有最中意的错误报告? 你最爱以哪种方式来检验一项新技术是否好用? 为什么词典被称作Dictionaries,而不是HashTable或HashMap?

优秀的个人博客,低调大师

面试官:说下你对方法区演变过程和内部结构的理解

之前我们已经了解过“运行时数据区”的程序计数器、虚拟机栈、本地方法栈和堆空间,今天我们就来了解一下最后一个模块——方法区。 简介 创建对象时内存分配简图 《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。所以,方法区可以看作是一块独立于 Java 堆的内存空间。 方法区与 Java 堆一样,是各个线程共享的内存区域。方法区在 JVM 启动时就会被创建,并且它的实际的物理内存空间是可以不连续的,关闭 JVM 就会释放这个区域的内存。 永久代、元空间 《java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit/IBM J9 中不存在永久代的概念。而对于 HotSpot 来说,在 jdk7 及以前,习惯上把方法区的实现称为永久代,而从 jdk8 开始,使用元空间取代了永久代。 方法区是 Java 虚拟机规范中的概念,而永久代和元空间是 HotSpot 虚拟机对方法区的一种实现。通俗点讲:如果把方法区比作接口的话,那永久代和元空间可以比作实现该接口的实现类。 直接内存 永久代、元空间并不只是名字变了,内部结构也进行了调整。永久代使用的是 JVM 的内存,而元空间使用的是本地的直接内存。 直接内存并不是 JVM 运行时数据区的一部分,因此不会受到 Java 堆的限制。但是它会受到本机总内存大小以及处理器寻址空间的限制,所以如果这部分内存也被频繁的使用,依然会导致 OOM 错误的出现。 方法区的大小 方法区的大小是可以进行设置的,可以选择固定大小也可以进行扩展。 jdk7 及以前 -XX:PermSize=N //方法区 (永久代) 初始分配空间,默认值为 20.75M -XX:MaxPermSize=N //方法区 (永久代) 最大可分配空间。32位机器默认是64M,64位机器默认是82M jdk8及以后 默认值依赖于平台,windows下: -XX:MetaspaceSize=N //方法区 (元空间) 初始分配空间,如果未指定此标志,则元空间将根据运行时的应用程序需求动态地重新调整大小。 -XX:MaxMetaspaceSize=N //方法区 (元空间) 最大可分配空间,默认值为 -1,即没有限制 与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,比如:加载大量的第三方 jar 包、Tomcat 部署的工程过多、大量动态生成反射类等都会导致方法区溢出,抛出内存溢出错误。 永久代:OutOfMemoryError:PermGen space 元空间:OutOfMemoryError:Metaspace 至于如何解决 OOM 异常,将在以后的文章中讲解! jvisualvm 我们可以通过 JDK 自带的 jvisualvm 工具来查看程序加载的类文件: 例 public class MethodAreaDemo1 { public static void main(String[] args) { System.out.println("start..."); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end..."); } } 运行程序,可以看到一个简单的程序就需要加载这么多的类文件。 高水位线 对于一个64位的服务器端 JVM 来说,XX:MetaspaceSize=21 就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。 新的高水位线的值取决于 GC 后释放了多少元空间: 如果释放的空间不足,那么在不超过 MaxMetaspaceSize 时,适当提高该值; 如果释放空间过多,则适当降低该值。 如果初始化的高水位线设置过低,高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到 Full GC 多次调用。为了避免频繁地GC,建议将 -XX :MetaspaceSize 设置为一个相对较高的值。 内部结构 《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。接下来我们就一起来看一下它的内部结构。 类型信息 对每个加载的类型( 类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息: 这个类型的完整有效名称(全名=包名.类名) 这个类型直接父类的完整有效名(对于 interface 或是 java. lang.Object ,都没有父类) 这个类型的修饰符( public , abstract, final 的某个子集) 这个类型直接接口的一个有序列表 域(Field)信息 JVM必须在方法区中保存类型的所有域(field,也称为属性)的相关信息以及域的声明顺序; 域的相关信息包括:域名称、 域类型、域修饰符(public, private,protected, static, final, volatile, transient 的某个子集) 方法(Method)信息 JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序: 方法名称 方法的返回类型(或void) 方法参数的数量和类型(按顺序) 方法的修饰符(public, private, protected, static, final,synchronized, native , abstract 的一个子集) 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract 和 native 方法除外) 异常表( abstract 和 native 方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引 non-final 的类变量 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分 类变量被类的所有实例所共享,即使没有类实例你也可以访问它。 我们可以通过例子来查看: public class MethodAreaDemo2 { public static void main(String[] args) { Order order = null; order.hello(); System.out.println(order.count); } } class Order { public static int count = 1; public static final int number = 2; public static void hello() { System.out.println("hello!"); } } 运行结果为: hello! 1 可以打开 IDEA 的 Terminal 窗口,在 MethodAreaDemo2.class 所在的路径下,输入 javap -v -p MethodAreaDemo2.class 命令 通过图片我们可以看出被声明为 final 的类变量的处理方法是不一样的,全局常量在编译的时候就被分配了。 运行时常量池 说到运行时常量池,我们先来了解一下什么是常量池表。 常量池表 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),里边存储着数量值、字符串值、类引用、字段引用和方法引用。 为什么字节码文件需要常量池? java 源文件中的类、接口,编译后会产生一个字节码文件。而字节码文件需要数据支持,通常这种数据会很大,以至于不能直接存放到字节码中。换一种方式,可以将指向这些数据的符号引用存到字节码文件的常量池中,这样字节码只需使用常量池就可以在运行时通过动态链接找到相应的数据并使用。 运行时常量池 运行时常量池( Runtime Constant Pool)是方法区的一部分,类加载器加载字节码文件时,将常量池表加载进方法区的运行时常量池。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。 运行时常量池,相对于 Class 文件常量池的另一重要特征是:具备动态性,比如 String.intern()。 演进细节 针对的是 Hotspot 的虚拟机: jdk1.6 及之前:有永久代 ,静态变量存放在永久代上; jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中; jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中; 演变示例图 为什么要将永久代替换为元空间呢? 永久代使用的是 JVM 的内存,受 JVM 设置的内存大小限制;元空间使用的是本地直接内存,它的最大可分配空间是系统可用内存的空间。因为元空间里存放的是类的元数据,所以随着内存空间的增大,能加载的类就更多了,相应的溢出的机率会大大减小。 在 JDK8,合并 HotSpot 和 JRockit 的代码时,JRockit 从来没有一个叫永久代的东西,合并之后就没有必要额外的设置这么一个永久代的地方了。 对永久代进行调优是很困难的。 StringTable 为什么要调整 因为永久代的回收效率很低,在 full gc 的时候才会触发。而 full GC 是老年代的空间不足、永久代不足时才会触发。这就导致了StringTable 回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。 垃圾回收 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。方法区的垃圾收集主要回收两部分内容:常量池中废奔的常量和不再使用的类型。 方法区内常量池中主要存放字面量和符号引用两大类常量: 字面量比较接近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值等。 符号引用则属于编译原理方面的概念,包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。 HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。 类型判定 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件: 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例; 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的; 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 Java 虛拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。 以上就是今天的全部内容了,如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

优秀的个人博客,低调大师

对话面试官:动态代理是如何实现的?JDK Proxy 和 CGLib 有啥区别?

2 --> 知识解读 动态代理的常用实现方式是反射。反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。 但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。 JDK Proxy 和 CGLib 的区别主要体现在以下几个方面: JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现; Java 对 JDK Proxy 提供了稳定的支持,并且会持续地升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多; JDK Proxy 是通过拦截器加反射的方式实现的; JDK Proxy 只能代理继承接口的类; JDK Proxy 实现和调用起来比较简单; CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高; CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。 重点分析 很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个: 你对 JDK Proxy 和 CGLib 的掌握程度。 Lombok 是通过反射实现的吗? 动态代理和静态代理有什么区别? 动态代理的使用场景有哪些? Spring 中的动态代理是通过什么方式实现的? 知识扩展 1.JDK Proxy 和 CGLib 的使用及代码分析 JDK Proxy 动态代理实现 JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示: import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * JDK Proxy 相关示例 */ public class ProxyExample { static interface Car { void running(); } static class Bus implements Car { @Override public void running() { System.out.println("The bus is running."); } } static class Taxi implements Car { @Override public void running() { System.out.println("The taxi is running."); } } /** * JDK Proxy */ static class JDKProxy implements InvocationHandler { private Object target; // 代理对象 // 获取到代理对象 public Object getInstance(Object target) { this.target = target; // 取得代理对象 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } /** * 执行代理方法 * @param proxy 代理对象 * @param method 代理方法 * @param args 方法的参数 * @return * @throws InvocationTargetException * @throws IllegalAccessException */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { System.out.println("动态代理之前的业务处理."); Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理) return result; } } public static void main(String[] args) { // 执行 JDK Proxy JDKProxy jdkProxy = new JDKProxy(); Car carInstance = (Car) jdkProxy.getInstance(new Taxi()); carInstance.running(); 以上程序的执行结果是: 动态代理之前的业务处理. The taxi is running. 可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下: public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。 CGLib 的实现 在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置: <!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> CGLib 实现代码如下: package com.lagou.interview; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CGLibExample { static class Car { public void running() { System.out.println("The car is running."); } } /** * CGLib 代理类 */ static class CGLibProxy implements MethodInterceptor { private Object target; // 代理对象 public Object getInstance(Object target) { this.target = target; Enhancer enhancer = new Enhancer(); // 设置父类为实例类 enhancer.setSuperclass(this.target.getClass()); // 回调方法 enhancer.setCallback(this); // 创建代理对象 return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("方法调用前业务处理."); Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用 return result; } } // 执行 CGLib 的方法调用 public static void main(String[] args) { // 创建 CGLib 代理类 CGLibProxy proxy = new CGLibProxy(); // 初始化代理对象 Car car = (Car) proxy.getInstance(new Car()); // 执行方法 car.running(); 以上程序的执行结果是: 方法调用前业务处理. The car is running. 可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。 2.Lombok 原理分析 在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效地解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,像这种方法都可以使用 Lombok 注解来完成。 例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的: public class Person { private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 在使用 Lombok 之后,代码是这样的: @Data public class Person { private Integer id; private String name; } 可以看出 Lombok 让代码简单和优雅了很多。 Tips:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示: 接下来讲讲 Lombok 的原理。 Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢? 回到我们刚才 Setter/Getter 的方法,当我们打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的: 可以看出 Lombok 是在编译期就为我们生成了对应的字节码。 其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下: 从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。 3.动态代理知识点扩充 动态代理和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。 动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。 Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-class=“true”/> 即可。 总结 本文介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。 除了 JDK Proxy 和 CGLib 之外,我们还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式,希望本文可以帮助到你。

优秀的个人博客,低调大师

面试官问如何结合Apollo构建动态线程池,我们聊了二十分钟

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习 0 文章概述 流量洪峰是互联网生产环境经常遇到的场景,例如某个时间点进行商品抢购活动,或者某个时间点集中触发定时任务,这些场景都有可能引发流量洪峰,所以如何应对流量洪峰是我们必须面对的问题。 纵向维度我们可以从代理层、WEB层、服务层、缓存层、数据层进行思考,横向维度我们可以从高频检测、缓存前置、节点冗余、服务降级等方向进行思考。本文我们从服务层动态调节线程数这个角度进行思考。 动态线程池是指我们可以根据流量的不同调节线程池某些参数,例如可以在业务低峰期调低线程数,在业务高峰期调高线程数增加处理线程从而应对流量洪峰。本文我们结合Apollo和线程池实现一个动态线程池。 1 线程池基础 1.1 七个参数 我们首先回顾一下Java线程池七大参数,查看源码ThreadPoolExecutor构造函数如下: publicclassThreadPoolExecutorextendsAbstractExecutorService{ publicThreadPoolExecutor(intcorePoolSize, intmaximumPoolSize, longkeepAliveTime, TimeUnitunit, BlockingQueue<Runnable>workQueue, ThreadFactorythreadFactory, RejectedExecutionHandlerhandler){ if(corePoolSize<0|| maximumPoolSize<=0|| maximumPoolSize<corePoolSize|| keepAliveTime<0) thrownewIllegalArgumentException(); if(workQueue==null||threadFactory==null||handler==null) thrownewNullPointerException(); this.acc=System.getSecurityManager()==null? null: AccessController.getContext(); this.corePoolSize=corePoolSize; this.maximumPoolSize=maximumPoolSize; this.workQueue=workQueue; this.keepAliveTime=unit.toNanos(keepAliveTime); this.threadFactory=threadFactory; this.handler=handler; } } (1) corePoolSize 线程池核心线程数,类比业务大厅开设的固定窗口。例如业务大厅开设2个固定窗口,那么这两个窗口不会关闭,全天都会进行业务办理 (2) workQueue 存储已提交但尚未执行的任务,类比业务大厅等候区。例如业务大厅一开门进来很多顾客,2个固定窗口进行业务办理,其他顾客到等候区等待 (3) maximumPoolSize 线程池可以容纳同时执行最大线程数,类比业务大厅最大窗口数。例如业务大厅最大窗口数是5个,业务员看到2个固定窗口和等候区都满了,可以临时增加3个窗口 (4) keepAliveTime 非核心线程数存活时间。当业务不忙时刚才新增的3个窗口需要关闭,空闲时间超过keepAliveTime空闲会被关闭 (5) unit keepAliveTime存活时间单位 (6) threadFactory 线程工厂可以用来指定线程名 (7) handler 线程池线程数已达到maximumPoolSize且队列已满时执行拒绝策略。例如业务大厅5个窗口全部处于忙碌状态且等候区已满,业务员根据实际情况选择拒绝策略 1.2 四种拒绝策略 (1) AbortPolicy 默认策略直接抛出RejectExecutionException阻止系统正常运行 /** *AbortPolicy * *@author微信公众号「JAVA前线」 * */ publicclassAbortPolicyTest{ publicstaticvoidmain(String[]args){ intcoreSize=1; intmaxSize=2; intqueueSize=1; AbortPolicyabortPolicy=newThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutorexecutor=newThreadPoolExecutor(coreSize,maxSize,1,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>(queueSize),Executors.defaultThreadFactory(),abortPolicy); for(inti=0;i<100;i++){ executor.execute(newRunnable(){ @Override publicvoidrun(){ System.out.println(Thread.currentThread().getName()+"->run"); } }); } } } 程序执行结果: pool-1-thread-1->run pool-1-thread-2->run pool-1-thread-1->run Exceptioninthread"main"java.util.concurrent.RejectedExecutionException:Taskcom.xy.juc.threadpool.reject.AbortPolicyTest$1@70dea4erejectedfromjava.util.concurrent.ThreadPoolExecutor@5c647e05[Running,poolsize=2,activethreads=0,queuedtasks=0,completedtasks=2] atjava.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) atjava.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) atjava.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) atcom.xy.juc.threadpool.reject.AbortPolicyTest.main(AbortPolicyTest.java:21) (2) CallerRunsPolicy 任务交给调用者自己运行 /** *CallerRunsPolicy * *@author微信公众号「JAVA前线」 * */ publicclassCallerRunsPolicyTest{ publicstaticvoidmain(String[]args){ intcoreSize=1; intmaxSize=2; intqueueSize=1; CallerRunsPolicycallerRunsPolicy=newThreadPoolExecutor.CallerRunsPolicy(); ThreadPoolExecutorexecutor=newThreadPoolExecutor(coreSize,maxSize,1,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>(queueSize),Executors.defaultThreadFactory(),callerRunsPolicy); for(inti=0;i<10;i++){ executor.execute(newRunnable(){ @Override publicvoidrun(){ System.out.println(Thread.currentThread().getName()+"->run"); } }); } } } 程序执行结果: main->run pool-1-thread-1->run pool-1-thread-2->run pool-1-thread-1->run main->run main->run pool-1-thread-2->run pool-1-thread-1->run main->run pool-1-thread-2->run (3) DiscardOldestPolicy 抛弃队列中等待最久的任务不会抛出异常 /** *DiscardOldestPolicy * *@author今日头条号「JAVA前线」 * */ publicclassDiscardOldestPolicyTest{ publicstaticvoidmain(String[]args){ intcoreSize=1; intmaxSize=2; intqueueSize=1; DiscardOldestPolicydiscardOldestPolicy=newThreadPoolExecutor.DiscardOldestPolicy(); ThreadPoolExecutorexecutor=newThreadPoolExecutor(coreSize,maxSize,1,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>(queueSize),Executors.defaultThreadFactory(),discardOldestPolicy); for(inti=0;i<10;i++){ executor.execute(newRunnable(){ @Override publicvoidrun(){ System.out.println(Thread.currentThread().getName()+"->run"); } }); } } } 程序执行结果: pool-1-thread-1->run pool-1-thread-2->run pool-1-thread-1->run (4) DiscardPolicy 直接丢弃任务不会抛出异常 /** *DiscardPolicy * *@author今日头条号「JAVA前线」 * */ publicclassDiscardPolicyTest{ publicstaticvoidmain(String[]args){ intcoreSize=1; intmaxSize=2; intqueueSize=1; DiscardPolicydiscardPolicy=newThreadPoolExecutor.DiscardPolicy(); ThreadPoolExecutorexecutor=newThreadPoolExecutor(coreSize,maxSize,1,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>(queueSize),Executors.defaultThreadFactory(),discardPolicy); for(inti=0;i<10;i++){ executor.execute(newRunnable(){ @Override publicvoidrun(){ System.out.println(Thread.currentThread().getName()+"->run"); } }); } } } 程序执行结果: pool-1-thread-1->run pool-1-thread-2->run pool-1-thread-1->run 1.3 修改参数 如果初始化线程池完成后,我们是否可以修改线程池某些参数呢?答案是可以。我们选择线程池提供的四个修改方法进行源码分析。 (1) setCorePoolSize publicclassThreadPoolExecutorextendsAbstractExecutorService{ publicvoidsetCorePoolSize(intcorePoolSize){ if(corePoolSize<0) thrownewIllegalArgumentException(); //新核心线程数减去原核心线程数 intdelta=corePoolSize-this.corePoolSize; //新核心线程数赋值 this.corePoolSize=corePoolSize; //如果当前线程数大于新核心线程数 if(workerCountOf(ctl.get())>corePoolSize) //中断空闲线程 interruptIdleWorkers(); //如果需要新增线程则通过addWorker增加工作线程 elseif(delta>0){ intk=Math.min(delta,workQueue.size()); while(k-->0&&addWorker(null,true)){ if(workQueue.isEmpty()) break; } } } } (2) setMaximumPoolSize publicclassThreadPoolExecutorextendsAbstractExecutorService{ publicvoidsetMaximumPoolSize(intmaximumPoolSize){ if(maximumPoolSize<=0||maximumPoolSize<corePoolSize) thrownewIllegalArgumentException(); this.maximumPoolSize=maximumPoolSize; //如果当前线程数量大于新最大线程数量 if(workerCountOf(ctl.get())>maximumPoolSize) //中断空闲线程 interruptIdleWorkers(); } } (3) setKeepAliveTime publicclassThreadPoolExecutorextendsAbstractExecutorService{ publicvoidsetKeepAliveTime(longtime,TimeUnitunit){ if(time<0) thrownewIllegalArgumentException(); if(time==0&&allowsCoreThreadTimeOut()) thrownewIllegalArgumentException("Corethreadsmusthavenonzerokeepalivetimes"); longkeepAliveTime=unit.toNanos(time); //新超时时间减去原超时时间 longdelta=keepAliveTime-this.keepAliveTime; this.keepAliveTime=keepAliveTime; //如果新超时时间小于原超时时间 if(delta<0) //中断空闲线程 interruptIdleWorkers(); } } (4) setRejectedExecutionHandler publicclassThreadPoolExecutorextendsAbstractExecutorService{ publicvoidsetRejectedExecutionHandler(RejectedExecutionHandlerhandler){ if(handler==null) thrownewNullPointerException(); //设置拒绝策略 this.handler=handler; } } 现在我们知道上述线程池调整参数的方法,但仅仅分析到此是不够的,因为如果没有动态调整参数的方法,那么每次修改必须重新发布才可以生效,那么有没有方法不用发布就可以动态调整线程池参数呢? 2 Apollo配置中心 2.1 核心原理 Apollo是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景,开源地址如下: https://github.com/ctripcorp/apollo 第一步用户在配置中心修改配置项,第二步配置中心通知Apollo客户端有配置更新,第三步Apollo客户端从配置中心拉取最新配置,更新本地配置并通知到应用,官网基础模型图如下: 配置中心配置项发生变化客户端如何感知呢?分为推和拉两种方式。推依赖客户端和服务端保持了一个长连接,发生数据变化时服务端推送信息给客户端,这就是长轮询机制。拉依赖客户端定时从配置中心服务端拉取应用最新配置,这是一个fallback机制。官网客户端设计图如下: 本文重点分析配置更新推送方式,我们首先看官网服务端设计图: ConfigService模块提供配置的读取推送等功能,服务对象是Apollo客户端。AdminService模块提供配置的修改发布等功能,服务对象是Portal模块即管理界面。需要说明Apollo并没有引用消息中间件,发送异步消息是指ConfigService定时扫描异步消息数据表: 消息数据保存在MySQL消息表: CREATETABLE`releasemessage`( `Id`int(11)unsignedNOTNULLAUTO_INCREMENTCOMMENT'自增主键', `Message`varchar(1024)NOTNULLDEFAULT''COMMENT'发布的消息内容', `DataChange_LastTime`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'最后修改时间', PRIMARYKEY(`Id`), KEY`DataChange_LastTime`(`DataChange_LastTime`), KEY`IX_Message`(`Message`(191)) )ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4COMMENT='发布消息' Apollo核心原理本文暂时分析到这里,后续我将写文章通过源码分析Apollo长轮询机制工作原理请继续关注。 2.2 实例分析 2.2.1 服务端安装 服务端关键步骤是导入数据库和修改端口号,具体步骤请参看官方网站: https://ctripcorp.github.io/apollo/#/zh/deployment/quick-start 启动成功后访问地址: http://localhost:8070 输入用户名apollo、密码admin: 进入我之前创建的myApp项目,我们看到在DEV环境、default集群、application命名空间包含一个timeout配置项: 2.2.2 应用程序 (1) 引入依赖 <dependencies> <dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>1.7.0</version> </dependency> </dependencies> (2) 简单实例 publicclassGetApolloConfigTestextendsBaseTest{ /** *-Dapp.id=myApp-Denv=DEV-Dapollo.cluster=default-Ddev_meta=http://localhost:8080 * *myApp+DEV+default+application */ @Test publicvoidtestGet()throwsInterruptedException{ ConfigappConfig=ConfigService.getAppConfig(); while(true){ Stringvalue=appConfig.getProperty("timeout","200"); System.out.println("timeout="+value); TimeUnit.SECONDS.sleep(1); } } } 因为上述程序是通过while(true)不断读取配置项的值,所以程序输出结果如下: timeout=100 timeout=100 timeout=100 timeout=100 timeout=100 timeout=100 现在把配置项的值改为200程序输出结果如下: timeout=100 timeout=100 timeout=100 timeout=100 timeout=200 timeout=200 timeout=200 (3) 监听实例 生产环境我们一般不用while(true)监听变化,而是通过注册监听器方式感知变化信息: publicclassGetApolloConfigTestextendsBaseTest{ /** *监听命名空间变化 * *-Dapp.id=myApp-Denv=DEV-Dapollo.cluster=default-Ddev_meta=http://localhost:8080 * *myApp+DEV+default+application */ @Test publicvoidtestListen()throwsInterruptedException{ Configconfig=ConfigService.getConfig("application"); config.addChangeListener(newConfigChangeListener(){ @Override publicvoidonChange(ConfigChangeEventchangeEvent){ System.out.println("发生变化命名空间="+changeEvent.getNamespace()); for(Stringkey:changeEvent.changedKeys()){ ConfigChangechange=changeEvent.getChange(key); System.out.println(String.format("发生变化key=%s,oldValue=%s,newValue=%s,changeType=%s",change.getPropertyName(),change.getOldValue(),change.getNewValue(),change.getChangeType())); } } }); Thread.sleep(1000000L); } } 我们把timeout值从200改为300,客户端可以监听到这个变化,程序输出结果如下: 发生变化命名空间=application 发生变化key=timeout,oldValue=200,newValue=300,changeType=MODIFIED 3 动态线程池 现在我们可以把线程池和Apollo结合起来构建动态线程池。首先我们用默认值构建一个线程池,然后线程池会监听Apollo相关配置项,如果相关配置有变化则刷新相关线程池参数。第一步在Apollo配置中心设置三个线程池参数(本文省略拒绝策略设置): 第二步编写核心代码: /** *动态线程池工厂 * *@author今日头条号「JAVA前线」 * */ @Slf4j @Component publicclassDynamicThreadPoolFactory{ privatestaticfinalStringNAME_SPACE="threadpool-config"; /**线程执行器**/ privatevolatileThreadPoolExecutorexecutor; /**核心线程数**/ privateIntegerCORE_SIZE=10; /**最大值线程数**/ privateIntegerMAX_SIZE=20; /**等待队列长度**/ privateIntegerQUEUE_SIZE=2000; /**线程存活时间**/ privateLongKEEP_ALIVE_TIME=1000L; /**线程名**/ privateStringthreadName; publicDynamicThreadPoolFactory(){ Configconfig=ConfigService.getConfig(NAME_SPACE); init(config); listen(config); } /** *初始化 */ privatevoidinit(Configconfig){ if(executor==null){ synchronized(DynamicThreadPoolFactory.class){ if(executor==null){ StringcoreSize=config.getProperty(KeysEnum.CORE_SIZE.getNodeKey(),CORE_SIZE.toString()); StringmaxSize=config.getProperty(KeysEnum.MAX_SIZE.getNodeKey(),MAX_SIZE.toString()); StringkeepAliveTIme=config.getProperty(KeysEnum.KEEP_ALIVE_TIME.getNodeKey(),KEEP_ALIVE_TIME.toString()); BlockingQueue<Runnable>queueToUse=newLinkedBlockingQueue<Runnable>(QUEUE_SIZE); executor=newThreadPoolExecutor(Integer.valueOf(coreSize),Integer.valueOf(maxSize),Long.valueOf(keepAliveTIme),TimeUnit.MILLISECONDS,queueToUse,newNamedThreadFactory(threadName,true),newAbortPolicyDoReport(threadName)); } } } } /** *监听器 */ privatevoidlisten(Configconfig){ config.addChangeListener(newConfigChangeListener(){ @Override publicvoidonChange(ConfigChangeEventchangeEvent){ log.info("命名空间发生变化={}",changeEvent.getNamespace()); for(Stringkey:changeEvent.changedKeys()){ ConfigChangechange=changeEvent.getChange(key); StringnewValue=change.getNewValue(); refreshThreadPool(key,newValue); log.info("发生变化key={},oldValue={},newValue={},changeType={}",change.getPropertyName(),change.getOldValue(),change.getNewValue(),change.getChangeType()); } } }); } /** *刷新线程池 */ privatevoidrefreshThreadPool(Stringkey,StringnewValue){ if(executor==null){ return; } if(KeysEnum.CORE_SIZE.getNodeKey().equals(key)){ executor.setCorePoolSize(Integer.valueOf(newValue)); log.info("修改核心线程数key={},value={}",key,newValue); } if(KeysEnum.MAX_SIZE.getNodeKey().equals(key)){ executor.setMaximumPoolSize(Integer.valueOf(newValue)); log.info("修改最大线程数key={},value={}",key,newValue); } if(KeysEnum.KEEP_ALIVE_TIME.getNodeKey().equals(key)){ executor.setKeepAliveTime(Integer.valueOf(newValue),TimeUnit.MILLISECONDS); log.info("修改活跃时间key={},value={}",key,newValue); } } publicThreadPoolExecutorgetExecutor(StringthreadName){ returnexecutor; } enumKeysEnum{ CORE_SIZE("coreSize","核心线程数"), MAX_SIZE("maxSize","最大线程数"), KEEP_ALIVE_TIME("keepAliveTime","线程活跃时间") ; privateStringnodeKey; privateStringdesc; KeysEnum(StringnodeKey,Stringdesc){ this.nodeKey=nodeKey; this.desc=desc; } publicStringgetNodeKey(){ returnnodeKey; } publicvoidsetNodeKey(StringnodeKey){ this.nodeKey=nodeKey; } publicStringgetDesc(){ returndesc; } publicvoidsetDesc(Stringdesc){ this.desc=desc; } } } /** *动态线程池执行器 * *@author今日头条号「JAVA前线」 * */ @Component publicclassDynamicThreadExecutor{ @Resource privateDynamicThreadPoolFactorythreadPoolFactory; publicvoidexecute(StringbizName,Runnablejob){ threadPoolFactory.getExecutor(bizName).execute(job); } publicFuture<?>sumbit(StringbizName,Runnablejob){ returnthreadPoolFactory.getExecutor(bizName).submit(job); } } 第三步运行测试用例: /** *动态线程池测试 * *@author今日头条号「JAVA前线」 * */ publicclassDynamicThreadExecutorTestextendsBaseTest{ @Resource privateDynamicThreadExecutordynamicThreadExecutor; /** *-Dapp.id=myApp-Denv=DEV-Dapollo.cluster=default-Ddev_meta=http://localhost:8080 * *myApp+DEV+default+thread-pool */ @Test publicvoidtestExecute()throwsInterruptedException{ while(true){ dynamicThreadExecutor.execute("bizName",newRunnable(){ @Override publicvoidrun(){ System.out.println("bizInfo"); } }); TimeUnit.SECONDS.sleep(1); } } } 第四步通过VisualVM观察线程数: 我们在配置中心修改配置项把核心线程数设置为50,最大线程数设置为100,通过VisualVM可以观察到线程数显著上升: 4 文章总结 本文我们首先介绍了线程池基础知识,包括七大参数和四个拒绝策略,随后我们介绍了Apollo配置中心的原理和应用,最后我们将线程池和配置中心相结合,实现了动态调整线程数的效果,希望本文对大家有所帮助。 欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

优秀的个人博客,低调大师

面试官问DUBBO不能降级哪类异常,我们聊了二十分钟

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流 1 服务雪崩 在分析服务降级之前,我们首先谈一谈什么是服务雪崩。现在我们假设存在A、B、C、D四个系统,系统间存在如下调用链路: 在正常情况下系统之间调用快速且正常,系统运行平稳。但是此时用户访问系统A的流量激增,这些流量在瞬间透传到B、C、D三个系统。B、C系统服务器节点较多抗住了这些流量,但是D系统服务器节点较少,没有抗住这些流量,导致D系统的资源逐渐耗尽,只能提供慢服务,最终结果是响应用户时延很长。 此时用户发现响应很慢,以为是自己网络不好会反复重试,那么成倍的流量会打到系统中,导致上游系统资源也逐渐耗尽了,整个访问链路都最终都不可用。 以上介绍了服务雪崩场景,我们发现在链路中一个节点出现问题,导致整个链路最终都不可用了,这是不可以接受的。 2 非线性 我们再从另一个概念来理解服务雪崩:非线性。这个概念在我们生活中无处不在。 你要赶早上8点钟的火车,如果6:30出发可以在7:00到达车站,于是你得到一个结论:只要30分钟就可以到达车站。 你早上想睡晚一点预计7:10出发,想着7:40可以到达车站。但是最可能的结果是你将错过这趟火车。因为正好遇上早高峰,堵车导致你至少需要花费1个小时才能到达车站。 一个小雪球的重量是100克,打雪仗时你被砸中100次,这对你不会造成任何影响。 但是如果你被10公斤的雪球砸中1次,这可能会对你造成严重的伤害。 这就是非线性。事物不是简单叠加关系,当达到某个临界值时会造成一种完全截然不同的结果。 我们来分析一个互联网的秒杀场景。假设你设计的秒杀系统当每秒30个人访问时,响应时间是10毫秒。即从用户点击按钮至得到结果这个过程,只花费了10毫秒。这个时间的流逝基本上察觉不到,性能是不错的。你感觉很好继续设计: 每秒30个访问量响应时间10毫秒 每秒300个访问量响应时间100毫秒 每秒3000个访问量响应时间1000毫秒 如果你按照这个思路去做系统设计,将会发生重大的错误。因为当每秒3000个访问量发生时,系统的响应时间可能不是1000毫秒,而可能直接导致系统崩溃,无法再处理任何的请求。最常见的场景就是当缓存系统失效时,导致的系统雪崩: (1) 当耗时低的缓存层出现故障时,流量直接打在了耗时高的数据库层,用户的等待时长就会增加 (2) 等待时长的增加导致用户更加频繁去访问,更多的流量会打在数据库层 (3) 这导致用户的等待时长进一步增加,再次导致更频繁的访问 (4) 当访问量达到一个极限值时,造成系统崩溃,无法再处理任何请求 流量和响应时间绝不是简单的叠加关系,当到达某个临界值时,技术系统将直接崩溃。 3 服务雪崩应对方案 保证系统的稳定性和高可用性,我们需要采取一些高可用策略,目的是构建一个稳定的高可用工程系统,我们一般采用如下方案。 3.1 冗余 + 自动故障转移 最基本的冗余策略就是主从模式。原理是准备两台机器,部署了同一份代码,在功能层面是相同的,都可以对外提供相同的服务。 一台机器启动提供服务,这就是主服务器。另一台机器启动在一旁待命,不提供服务,随时监听主服务器的状态,这就是从服务器。当发现主服务器出现故障时,从服务器立刻替换主服务器,继续为用户提供服务。 自动故障转移策略是指当主系统发生异常时,应该可以自动探测到异常,并自动切换为备用系统。不应该只依靠人工去切换成,否则故障处理时间会显著增加。 3.2 降级策略 所谓降级策略,就是当系统遇到无法承受的压力时,选择暂时关闭一些非关键的功能,或者延时提供一些功能,把此刻所有的资源都提供给现在最关键的服务。 在秒杀场景中下订单就是最核心最关键的功能。当系统压力将要到达临界值时,可以暂时先关闭一些非核心功能如查询功能。 当秒杀活动结束后,再将暂时关闭的功能开启。这样既保证了秒杀活动的顺利进行,也保护了系统没有崩溃。 还有一种降级策略,当系统依赖的下游服务出现错误,甚至已经完全不可用了,那么此时就不能再调用这个下游服务了,否则可能导致雪崩。所以直接返回兜底方案,把下游服务直接降级。 这里比较两个概念:服务降级与服务熔断,因为这两个概念比较相似。我认为服务熔断是服务降级的一个方法,而服务降级还有很多其它方法,例如开关降级、流量降级等等。 3.3 延时策略 用户下订单成功后就需要进行支付。假设秒杀系统下订单每秒访问量是3000,我们来思考一个问题,有没有必要将每秒3000次访问量的压力传递给支付服务器? 答案是没有必要。因为用户秒杀成功后可以稍晚付款,比如可以跳转到一个支付页面,提示用户只要在10分钟内支付完成即可。 这样每秒3000次访问量就被分摊至几分钟,有效保护了系统。技术架构还可以使用消息队列做缓冲,让支付服务按照自己的能力去处理业务。 3.4 隔离策略 物理隔离:应用分别部署在不同物理机、不同机房,资源不会互相影响。 线程隔离:不同类型的请求进行分类,交给不同的线程池处理,当一类请求出现高耗时和异常,不影响另一类请求访问。 4 服务降级 本文我们重点结合Dubbo框架谈一谈服务降级。现在我们有服务提供者提供如下服务: publicinterfaceHelloService{ publicStringsayHello(Stringname)throwsException; } publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ Stringresult="hello["+name+"]"; returnresult; } } 配置文件声明服务接口: <dubbo:serviceinterface="com.java.front.demo.provider.HelloService"ref="helloService"/> 4.1 降级策略配置 Dubbo框架是自带服务降级策略的,提供了三种常用的降级策略,我们看一看如何进行配置。 (1) 强制降级策略 <dubbo:referenceid="helloService"mock="force:return1"interface="com.java.front.demo.provider.HelloService"/ (2) 异常降级策略 <dubbo:referenceid="helloService"mock="throwcom.java.front.BizException"interface="com.java.front.dubbo.demo.provider.HelloService"/> (3) 自定义降级策略 packagecom.java.front.dubbo.demo.consumer; importcom.java.front.demo.provider.HelloService; publicclassHelloServiceMockimplementsHelloService{ @Override publicStringsayHello(Stringname)throwsException{ return"mock"; } } 配置指定自定义降级策略: <dubbo:referenceid="helloService"mock="com.java.front.dubbo.demo.consumer.HelloServiceMock"interface="com.java.front.demo.provider.HelloService"/> 4.2 源码分析 publicclassMockClusterInvoker<T>implementsInvoker<T>{ @Override publicResultinvoke(Invocationinvocation)throwsRpcException{ Resultresult=null; //检查是否有mock属性 Stringvalue=directory.getUrl().getMethodParameter(invocation.getMethodName(),Constants.MOCK_KEY,Boolean.FALSE.toString()).trim(); //没有mock属性直接执行消费逻辑 if(value.length()==0||value.equalsIgnoreCase("false")){ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); } //不执行消费逻辑直接返回 elseif(value.startsWith("force")){ if(logger.isWarnEnabled()){ logger.warn("force-mock:"+invocation.getMethodName()+"force-mockenabled,url:"+directory.getUrl()); } //直接执行mock逻辑 result=doMockInvoke(invocation,null); }else{ try{ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); }catch(RpcExceptione){ if(e.isBiz()){ throwe; } if(logger.isWarnEnabled()){ logger.warn("fail-mock:"+invocation.getMethodName()+"fail-mockenabled,url:"+directory.getUrl(),e); } //服务消费失败执行mock逻辑 result=doMockInvoke(invocation,e); } } returnresult; } } publicclassMockInvoker<T>implementsInvoker<T>{ @Override publicResultinvoke(Invocationinvocation)throwsRpcException{ Stringmock=getUrl().getParameter(invocation.getMethodName()+"."+Constants.MOCK_KEY); if(invocationinstanceofRpcInvocation){ ((RpcInvocation)invocation).setInvoker(this); } if(StringUtils.isBlank(mock)){ mock=getUrl().getParameter(Constants.MOCK_KEY); } if(StringUtils.isBlank(mock)){ thrownewRpcException(newIllegalAccessException("mockcannotbenull.url:"+url)); } mock=normalizeMock(URL.decode(mock)); //<mock="force:return1">直接包装返回结果 if(mock.startsWith(Constants.RETURN_PREFIX)){ mock=mock.substring(Constants.RETURN_PREFIX.length()).trim(); try{ Type[]returnTypes=RpcUtils.getReturnTypes(invocation); Objectvalue=parseMockValue(mock,returnTypes); returnnewRpcResult(value); }catch(Exceptionew){ thrownewRpcException("mockreturninvokeerror.method:"+invocation.getMethodName()+",mock:"+mock+",url:"+url,ew); } } //<mock="throw">抛出异常 elseif(mock.startsWith(Constants.THROW_PREFIX)){ mock=mock.substring(Constants.THROW_PREFIX.length()).trim(); if(StringUtils.isBlank(mock)){ thrownewRpcException("mockedexceptionforservicedegradation."); }else{ //获取自定义异常 Throwablet=getThrowable(mock); thrownewRpcException(RpcException.BIZ_EXCEPTION,t); } } //<mock="com.java.front.HelloServiceMock">自定义mock策略 else{ try{ Invoker<T>invoker=getInvoker(mock); returninvoker.invoke(invocation); }catch(Throwablet){ thrownewRpcException("Failedtocreatemockimplementationclass"+mock,t); } } } } 5 产生疑问 通过上述源码我们知道,如果在mock属性中配置force,那么不会执行真正的业务逻辑,而是只执行mock逻辑,这一部分比较容易理解: //不执行消费逻辑直接返回 elseif(value.startsWith("force")){ if(logger.isWarnEnabled()){ logger.warn("force-mock:"+invocation.getMethodName()+"force-mockenabled,url:"+directory.getUrl()); } //直接执行mock逻辑 result=doMockInvoke(invocation,null); } 但是如果是其它mock配置则首先执行业务代码,如果业务代码发生异常了再执行mock逻辑: try{ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); }catch(RpcExceptione){ if(e.isBiz()){ throwe; } if(logger.isWarnEnabled()){ logger.warn("fail-mock:"+invocation.getMethodName()+"fail-mockenabled,url:"+directory.getUrl(),e); } //服务消费失败执行mock逻辑 result=doMockInvoke(invocation,e); } 这段代码捕获了RpcException异常,那么问题来了RpcException是什么类型的异常?我们使用自定义降级策略进行实验,消费者代码如下: packagecom.java.front.dubbo.demo.consumer; importcom.java.front.demo.provider.HelloService; publicclassHelloServiceMockimplementsHelloService{ @Override publicStringsayHello(Stringname)throwsException{ return"mock"; } } 配置指定自定义策略并设置服务超时为2秒: <dubbo:referenceid="helloService"mock="com.java.front.dubbo.demo.consumer.HelloServiceMock"interface="com.java.front.demo.provider.HelloService"timeOut="2000"/> 消费者测试代码如下: publicstaticvoidtestMock(){ ClassPathXmlApplicationContextcontext=newClassPathXmlApplicationContext(newString[]{"classpath*:META-INF/spring/dubbo-consumer1.xml"}); context.start(); HelloServicehelloServiceMock=(HelloService)context.getBean("helloService"); Stringresult=helloServiceMock.sayHello("JAVA前线"); System.out.println("消费者收到结果="+result); } 5.1 超时异常 5.1.1 代码实例 我们在生产者业务代码造成5秒的阻塞,模拟一个慢服务: publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ Stringresult="hello["+name+"]"; //模拟耗时操作5秒 Thread.sleep(5000L); returnresult; } } 消费者执行返回mock结果,说明超时异常属于RpcException异常,可以被降级策略捕获: 消费者收到结果=mock 5.1.2 源码分析 要分析超时异常为什么可以被降级策略捕获,我们从以下两个类分析。DefaultFuture.get方法采用了经典多线程保护性暂停模式,并且实现了异步转同步的效果,如果发生超时异常则抛出TimeoutException异常: publicclassDefaultFutureimplementsResponseFuture{ @Override publicObjectget(inttimeout)throwsRemotingException{ if(timeout<=0){ timeout=Constants.DEFAULT_TIMEOUT; } //response对象为空 if(!isDone()){ longstart=System.currentTimeMillis(); lock.lock(); try{ //进行循环 while(!isDone()){ //放弃锁并使当前线程阻塞,直到发出信号或中断它或者达到超时时间 done.await(timeout,TimeUnit.MILLISECONDS); //阻塞结束后再判断是否完成 if(isDone()){ break; } //阻塞结束后判断超过超时时间 if(System.currentTimeMillis()-start>timeout){ break; } } }catch(InterruptedExceptione){ thrownewRuntimeException(e); }finally{ lock.unlock(); } //response对象仍然为空则抛出超时异常 if(!isDone()){ thrownewTimeoutException(sent>0,channel,getTimeoutMessage(false)); } } returnreturnFromResponse(); } } DubboInvoker调用了DefaultFuture.get方法,如果捕获到上述TimeoutException则会抛出RpcException: publicclassDubboInvoker<T>extendsAbstractInvoker<T>{ @Override protectedResultdoInvoke(finalInvocationinvocation)throwsThrowable{ try{ //request方法发起远程调用->get异步转同步并进行超时验证 RpcContext.getContext().setFuture(null); Resultresult=(Result)currentClient.request(inv,timeout).get(); returnresult; }catch(TimeoutExceptione){ thrownewRpcException(RpcException.TIMEOUT_EXCEPTION,"Invokeremotemethodtimeout.method:"+invocation.getMethodName()+",provider:"+getUrl()+",cause:"+e.getMessage(),e); }catch(RemotingExceptione){ thrownewRpcException(RpcException.NETWORK_EXCEPTION,"Failedtoinvokeremotemethod:"+invocation.getMethodName()+",provider:"+getUrl()+",cause:"+e.getMessage(),e); } } } 源码分析到这里已经很清楚了,RpcException正是服务降级策略可以捕获的异常,所以超时异常是可以被降级的。 5.2 业务异常 本文我们把非超时异常统称为业务异常,例如生产者业务执行时发生运行时异常可以归为业务异常,下面我们进行试验。 5.2.1 代码实例 生产者执行过程中抛出运行时异常: publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ thrownewRuntimeException("BizException") } } 消费者调用直接抛出异常: java.lang.RuntimeException:BizException atcom.java.front.dubbo.demo.provider.HelloServiceImpl.sayHello(HelloServiceImpl.java:35) atorg.apache.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) atorg.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:56) atorg.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:85) 5.2.2 源码分析 我们发现服务降级对业务异常没有生效,需要分析原因,我认为从以下两点进行分析: (1) 消费者接收到什么消息 publicclassDefaultFutureimplementsResponseFuture{ publicstaticvoidreceived(Channelchannel,Responseresponse){ try{ DefaultFuturefuture=FUTURES.remove(response.getId()); if(future!=null){ future.doReceived(response); }else{ logger.warn("Thetimeoutresponsefinallyreturnedat" +(newSimpleDateFormat("yyyy-MM-ddHH:mm:ss.SSS").format(newDate())) +",response"+response +(channel==null?"":",channel:"+channel.getLocalAddress() +"->"+channel.getRemoteAddress())); } }finally{ CHANNELS.remove(response.getId()); } } } response用来接收服务端发送的消息,我们看到异常信息存放在Response的exception属性: Response[id=0,version=null,status=20,event=false,error=null,result=RpcResult[result=null,exception=java.lang.RuntimeException:BizException]] (2) 异常在哪里被抛出 我们知道消费者对象是一个代理对象,首先会执行到InvokerInvocationHandler: publicclassInvokerInvocationHandlerimplementsInvocationHandler{ privatefinalInvoker<?>invoker; publicInvokerInvocationHandler(Invoker<?>handler){ this.invoker=handler; } @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ StringmethodName=method.getName(); Class<?>[]parameterTypes=method.getParameterTypes(); if(method.getDeclaringClass()==Object.class){ returnmethod.invoke(invoker,args); } if("toString".equals(methodName)&&parameterTypes.length==0){ returninvoker.toString(); } if("hashCode".equals(methodName)&&parameterTypes.length==0){ returninvoker.hashCode(); } if("equals".equals(methodName)&&parameterTypes.length==1){ returninvoker.equals(args[0]); } //RpcInvocation[methodName=sayHello,parameterTypes=[classjava.lang.String],arguments=[JAVA前线],attachments={}] RpcInvocationrpcInvocation=createInvocation(method,args); //消费者Invoker->MockClusterInvoker(FailoverClusterInvoker(RegistryDirectory(invokers))) Resultresult=invoker.invoke(rpcInvocation); //结果包含异常信息则抛出异常->例如异常结果对象RpcResult[result=null,exception=java.lang.RuntimeException:sayHelloError1error] returnresult.recreate(); } } RpcResult.recreate方法会处理异常,如果发现异常对象不为空则抛出异常: publicclassRpcResultextendsAbstractResult{ @Override publicObjectrecreate()throwsThrowable{ if(exception!=null){ try{ Classclazz=exception.getClass(); while(!clazz.getName().equals(Throwable.class.getName())){ clazz=clazz.getSuperclass(); } FieldstackTraceField=clazz.getDeclaredField("stackTrace"); stackTraceField.setAccessible(true); ObjectstackTrace=stackTraceField.get(exception); if(stackTrace==null){ exception.setStackTrace(newStackTraceElement[0]); } }catch(Exceptione){ } throwexception; } returnresult; } } 5.2.3 业务异常如何降级 通过上述实例我们知道Dubbo自带的服务降级策略只能降级超时异常,而不能降级业务异常。 那么业务异常应该如何降级呢?我们可以整合Dubbo、Hystrix进行业务异常熔断,相关配置也并不复杂,大家可以网上查阅相关资料。 6 文章总结 本文我们首先介绍了服务雪崩这个场景,并且从非线性角度再次理解了服务雪崩。随后我们总结了服务雪崩应对方案,其中服务降级是应对服务雪崩的重要方法之一。我们针对超时异常和业务异常两种场,结合源码深入分析了Dubbo服务降级的使用场景,希望本文对大家有所帮助。 欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流

优秀的个人博客,低调大师

一文带你掌握Vue3新特性,再也不怕面试官啦~

前言 记录一下笔记,给各位小伙伴们分享一些常用的Vue3新特性及哪些方法发生了变更。 我这里只写一些常用的,更多请看Vue3官网这里 > 有兴趣的可以关注公众号:前端娱乐圈 组件v-model支持参数 在Vue2.x时,我们要想给子组件传值,还得单独传入。Vue3.x直接以v-model:xxx形式传入参数,并且配合修饰符.sync进行数据同步更新。 App.vue <template> <div> {{title}} <input v-model:title="title"> </div> </template> <script> import Input from "./components/Input" export default { name: 'App', components: { Input }, data() { return { title: "蛙人", } }, } </script> Input.vue <template> <div class="Input"> <input type="text" @input="first" :value="title"> </div> </template> <script> export default { name: 'Input', props: { title: { default: () => "蛙人" } }, methods: { first(e) { this.$emit("update:title", e.target.value) }, }, } </script> 效果 组件支持多个v-model 在Vue3.x中支持在单个组件上可以创建多个v-model绑定。 App.vue <template> {{title}} {{name}} <input v-model:title="title" v-model:name="name"> </template> <script> import Input from "./components/Input" export default { name: 'App', components: { Input, }, data() { return { title: "蛙人", name: "www" } }, } </script> Input.vue <template> <div class="Input"> <input type="text" @input="first" :value="title"> <input type="text" @input="last" :value="name"> </div> </template> <script> export default { name: 'Input', props: { title: { default: () => "蛙人" }, name: { default: () => "前端娱乐圈" } }, methods: { first(e) { this.$emit("update:title", e.target.value) }, last(e) { this.$emit("update:name", e.target.value) } } } </script> 效果 Setup setup函数是一个新的组件选项。作为在组件内使用Composition Api的入口点。下面我们分为4个方面来讲解它 调用时机 this指向 函数参数 返回值 调用时机 创建组件实例,然后初始化props,紧接着就调用setup函数。 从生命周期的角度来看,它会在beforeCreate之前执行。也就是创建组件先执行setup、beforeCreate、create。 <template> <div>蛙人</div> </template> <script> export default { name: 'App', setup() { console.log("hey 蛙人") } } </script> this指向 由于不能在setup函数中使用data、methods,为了避免使用Vue出错,所以把setup函数中this修改为了undefined。 <template> <div>蛙人</div> </template> <script> export default { name: 'App', setup() { console.log(this); // undefined } } </script> 函数参数 props context props 接收组件传递过来的所有数据,并且都是响应式的。 <template> <div>蛙人</div> </template> <script> export default { name: 'App', props: { title: { type: String } }, setup(props) { console.log(props.title) } } </script> > 注意一点,props数据不能使用解构,否则响应式数据失效 <template> <div>蛙人</div> </template> <script> export default { name: 'App', props: { title: { type: String } }, setup({ title }) { console.log(title) // 这里响应式数据将失效 } } </script> context 该参数提供一个上下文对象,从原来的2.x中选择性的暴露了一些属性。 attrs slots emit <template> <div>蛙人</div> </template> <script> export default { name: 'App', props: { title: { type: String } }, setup(props, { attrs, slots, emit } ) { console.log(attrs) } } </script> 上面,attrs和slots都是内部组件实例上对应项的代理,可以确保在更新后仍然还是最新的值。所以这里可以使用解构语法。 返回值 可以将setup函数返回值渲染到页面上。但前提是,setup返回值必须是一个对象,否则返回其它值则渲染无效。 <template> <div>蛙人</div> </template> <script> export default { name: 'App', props: { title: { type: String } }, setup() { const name = "蛙人" return { name } } } </script> Reactive 该方法接收一个参数{}创建一个响应式对象。跟Vue2.x的Vue.observable一样。如果该参数不是对象的话,也可以渲染到模板上,但不是响应式的数据。 <template> <div class="test"> 姓名: {{ name.value }} {{ test() }} </div> </template> <script> import { reactive } from "vue" export default { name: 'test', data() { return { } }, setup() { let name = reactive({value: "蛙人"}) function test() { name.value = "abc"; // 该方法测试响应式数据,可以看到执行完该方法视图也会发生改变 } return { name, test } } } </script> Ref 该方法接收一个参数,可以是单个值,也可以是一个对象,并且都是响应式的数据。当传入一个对象时{},内部将调用reactive方法进行转换为响应式数据。返回值里面带有.value属性取值,当使用模板渲染的时候可省去.value。 <template> <div class="test"> 姓名: {{ name }} {{ test() }} </div> </template> <script> import { ref } from "vue" export default { name: 'test', data() { return { } }, setup() { let name = ref("蛙人") function test() { name.value = "abc"; // 只是渲染模板可以省略.value,但是在逻辑中还得写哦 } return { name, test } } } </script> Computed 该方法可以传入一个函数,默认该函数就是getter,不管getter返回值为一个ref响应式数据还是一个普通变量,数据都是只读不能改变。 <script> import { ref, computed } from "vue" export default { name: 'test', setup() { let name = ref("蛙人") let test = computed(() => name.value); test.value = "123" // 修改无效,只能只读 } } </script> 传入一个对象set和get函数方法,这样就可以修改啦 <script> import { ref, computed } from "vue" export default { name: 'test', setup() { let name = ref("蛙人") let test = computed({ get() { return name.value; }, set(val) { return name.value = val; } }); test.value = "123" } } </script> Readonly 该方法接收传入一个对象,默认是只读功能,是深层对象只读,不管嵌套多少层的属性都是只读状态。 <script> import { readonly } from "vue" export default { name: 'test', setup() { let obj = { name: "蛙人", sex: "male", prodution: { proName: "音响" } } let only = readonly(obj) only.name = "前端娱乐圈" // 修改无效 only.prodution.proName = "欢迎关注" // 修改无效 console.log(only) } } </script> WatchEffect 该方法接收一个函数并且立即执行,并当该函数里的变量变更时,重新执行该函数。该方法无法获取到原值,只能是改变之后的值。 > 如果要监听哪个值,需要在该函数里写出来,否则监听无效 import { ref, watchEffect } from "vue" export default { name: 'test', setup() { let name = ref("蛙人"); let age = ref(23); watchEffect(() =&gt; { name.value; // 监听name age.value; // 监听age console.log(name.value) console.log(age.value) }) setTimeout(() =&gt; { name.value = "前端娱乐圈" }, 5000) setTimeout(() =&gt; { age.value = 18 }, 1000) } } 取消监听 有时候我们想在触发一定的条件后取消监听。这时可以执行watchEffect的返回值。 import { ref, watchEffect } from "vue" export default { name: 'test', setup() { let name = ref("蛙人"); let age = ref(23); let stop = watchEffect(() =&gt; { name.value; // 监听name age.value; // 监听age console.log(name.value) console.log(age.value) }) setTimeout(() =&gt; { name.value = "前端娱乐圈" }, 5000) setTimeout(() =&gt; { age.value = 18; setTimeout(stop, 300) }, 1000) } } Watch watch等同于Vue2.x中的this.$watch,watch需要监听特定数据,默认情况是懒执行,也就是只有当数据发生变化时才执行第二个参数函数。 对比WatchEffect ,Watch允许我们 懒执行函数 更明确哪些状态改变触发监听器 可以监听获取到变化前后的值 监听单个值 <script> import { ref, watch } from "vue" export default { name: 'test', setup() { let name = ref("蛙人"); watch(name, (newVal, oldVal) => { console.log(newVal, oldVal) // 前端娱乐圈, 蛙人 }) setTimeout(() => { name.value = "前端娱乐圈" }, 1000) } } </script> 监听多个值 监听多个值,返回的是一个数组对象。 <script> import { ref, watch } from "vue" export default { name: 'test', setup() { let name = ref("蛙人"); let age = ref(23); watch([name, age], (newVal, oldVal) => { console.log(newVal, oldVal) // ["前端娱乐圈", 18], ["蛙人", 23] }) setTimeout(() => { name.value = "前端娱乐圈" age.value = 18 }, 1000) } } </script> 生命周期系列 在Vue3.X也可以在setup函数下使用生命周期,这些钩子函数写法跟之前的生命周期写法不同。 > 注意:这些生命周期写法只能在setup函数下使用,在其它地方使用则会报错。 与Vue2.x版本生命周期相对应的组合式Api beforeCreate --> setup created --> setup beforeMount --> onBeforeMount mounted --> onMounted beforeUpdate --> onBeforeUpdate updated --> onUpdated beforeDestroy --> onBeforeUnmount destroyed --> onUnmount 下面我们来看一下这些钩子的写法。钩子函数里面是一个回调函数。 <script> import { onMounted, onUpdated, onUnmounted } from "vue" export default { name: 'test', setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) } } </script> Provide && Inject 该方法和Vue2.x的 provide、inject一样的。但是Vue3新特性这俩方法只能在setup中使用。 Provide:接收2个参数,第一个key值,第二个value值,进行传递 Inject:接收2个参数,第一个是provide的key值,默认第二个参数可选,可以设置默认值(当找不到key值,设置一个默认值) App.vue <script> import test from "./components/test" import { provide, ref } from "vue" export default { name: 'App', components: { test }, setup() { let name = ref("蛙人") provide("name", name) // 传入一个响应式数据 }, } </script> test.vue <template> {{ NAME }} </template> <script> import { inject } from "vue" export default { name: 'test', setup() { let NAME = inject("name") console.log(NAME) // 蛙人 let title = inject("key", 123) console.log(title) // 这时就会触发默认值,因为这里找不到这个key值 return { NAME } } } </script> Refs 该方法相当于Vue2.x的refs一样获取元素,那么在setup中配合使用ref对象进行获取 <template> <div class="test"> <p ref="el">123</p> </div> </template> <script> import { ref, onMounted } from "vue" export default { name: 'test', setup() { let el = ref(null) onMounted(() => { console.log(el) // p标签元素 }) return { el } } } </script> isReadonly 用于检测该数据是不是可读数据。返回一个Boolean类型。 <script> import { isReadonly, readonly} from "vue" export default { name: 'test', setup() { let test = readonly({name: "蛙人"}) console.log(isReadonly(test)) // true let test2 = readonly("蛙人") console.log(isReadonly(test2)) // false, 这不是一个只读数据 } } </script> isRef 用于检测该数据是不是ref响应式数据。返回一个Boolean类型。 <script> import { isRef, ref } from "vue" export default { name: 'test', setup() { let test = ref("公众号:前端娱乐圈") console.log(isRef(test)) // true } } </script> isReactive 用于检测该数据是不是reacttive响应式数据。返回一个Boolean类型。 <script> import { isReactive, reactive } from "vue" export default { name: 'test', setup() { let test = reactive({name: "蛙人"}) console.log(isReactive(test)) // true let test2 = reactive("蛙人") console.log(isReactive(test2)) // false, 这不是一个响应式数据 } } </script> 移除过滤器filters 在Vue3.x中移除过滤器,不在支持。建议使用computed去替代。贴一个官网例子 <template> <h1>Bank Account Balance</h1> <p>{{ accountInUSD }}</p> </template> <script> export default { props: { accountBalance: { type: Number, required: true } }, computed: { accountInUSD() { return '$' + this.accountBalance } } } </script> 不再限制Template一个根节点 Vue3.x中将不在限制模板中只有一个根节点,根组件可以任意多个元素。 <template> <div>首页</div> <div>新闻</div> </template> 自定义v-model修饰符 Vue3.x中,添加了可以自定义修饰符,如Api提供的内置方法.trim,新特性我们也可以自定义啦。下面就来演示一下写一个转换字符串大写的修饰符。 App.vue <template> <input v-model:str.capitalize="modelModifiers"> </template> <script> import Input from "./components/Input" export default { name: 'App', components: { Input } } </script> Input.vue <template> <div class="Input"> <input type="text" @input="send"> </div> </template> <script> export default { name: 'Input', props: { str: String, strModifiers: { default: () => ({}) } }, methods: { send(e) { let value = e.target.value if (this.strModifiers.capitalize) { // capitalize 这里的值就是修饰符 value = value.toUpperCase() console.log(value) } this.$emit("update:str", value) } } } </script> 上面方法,必须传入props值,Modifiers必须写定义为一个空对象。 > 特别需要注意一点:如果你的v-model参数值为str,那么组件里面接收的值,全部为str开头,如:props里面的 strModifiers,str 效果: 废弃on,off,once实例方法 Vue3.x中 $on,$off 和 $once 实例方法已被移除,应用实例不再实现事件触发接口。 <script> created() { console.log(this.$on, this.$once, this.$off) // undefined undefined undefined } </script> 自定义指令更改 在Vue3.x中自定义指定写法稍有更改,看下列。 bind --> beforeMount 指令绑定到元素后发生。只发生一次 inserted --> mounted 元素插入到父DOM后发生 beforeUpdate: Vue3.x新添加的,这是在元素更新之前调用, componentUpdated --> updated beforeUnmount : Vue3.x新添加的,将在卸载元素前调用 unbind --> unmounted main.js import { createApp } from 'vue' import App from './App.vue' let main = createApp(App) main.directive("custom", { beforeMount(el, attr) { console.log(el, attr) }, updated() { console.log("updated") }, unmounted() { console.log("移除") } }) main.mount('#app') App.vue <template> <p v-custom v-if="show"></p> </template> <script> export default { name: 'App', data() { return { show: true } }, created() { setTimeout(() => { this.show = true; }, 5000) setTimeout(() => { this.show = false }, 3000) } } </script> 感谢 谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。 我是蛙人(✿◡‿◡),如果觉得写得可以的话,请点个赞吧❤。 感兴趣的小伙伴可以加入 [ 前端娱乐圈交流群 ] 欢迎大家一起来交流讨论 写作不易,「点赞」+「在看」+「转发」 谢谢支持❤ 往期好文 《聊聊在Vue项目中使用Decorator装饰器》 《分享15个Webpack实用的插件!!!》 《手把手教你写一个Vue组件发布到npm且可外链引入使用》 《分享12个Webpack中常用的Loader》 《聊聊什么是CommonJs和Es Module及它们的区别》 《这些工作中用到的JavaScript小技巧你都知道吗?》 《【建议收藏】分享一些工作中常用的Git命令及特殊问题场景怎么解决》 参考 https://v3.cn.vuejs.org/

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。