spring源码学习之设计模式(1)单例模式
众所周知,单例模式分为饿汉式和懒汉式,昨天在看了《spring5核心原理与30个类手写实战》之后才知道饿汉式有很多种写法,分别适用于不同场景,避免反射,线程不安全问题。下面就各种场景、采用的方式及其优缺点介绍。
饿汉式 (绝对的线程安全)
代码示例
1.第一种写法 ( 定义即初始化)
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 第二种写法 (静态代码块)
public class Singleton{
private static final Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
饿汉式基本上就这两种写法。在spring框架中IoC的ApplicantsContext就是使用的饿汉式单例,保证了全局只有一个ApplicationContext 和在应用启动后就能获取实例,一遍进行接下来的操作.
优点
因其在程序启动后就已经初始化,也不需要任何锁保证线程安全 ,所以执行效率高
缺点
因为在程序启动后就已经进行了初始化,即便是不用也进行了初始化,所以无论何时都占用内存空间,浪费了内存空间。
懒汉式 (线程安全需要另外的操作)
##### 代码示例
第一种写法
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
上面的代码不难看出,在单线程下执行是没有问题的,但在多线程情况下,线程执行速度和顺序无法控制确定,故有可能会产生多个实例对象,这样就违背了单例模式的初衷了。
第二种写法
加锁保证线程安全(synchronized关键字)
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
可以看到在getInstance()上加了synchronized关键字,就能保证线程同步。但又有一个问题:使用synchronized关键字是,当一个线程调用获取实例的方法时,会锁住整个类,其他的线程再调用,会使线程状态由 RUNNING 变成 MONITOR ,进而导致线程阻塞,执行效率下降;知道这个线程执行完实例方法,其他线程才能继续执行,两个线程时,效率下降还在可以接受范围内,但在实际应用场景中,使用线程池来管理线程的调度,会有大量的线程,如果这些线程都阻塞了,其结果可以预见。
上述问题有什么更好的问题解决呢?使用双重检查锁机制可以完美的解决这个问题。其代码如下
public class Singleton{
private volatile static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
synchronized(Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里需要解释下,童鞋们都知道一个对象使用要经历一下步骤:
为对象分配内存
初始化对象
实例对象指向第一布分配的内存地址
在java中JVM为了提高执行效率,会进行指令重排。那什么时指令重排呢?指令重排是指JVM为了优化指令,提高程序的运行效率,在不影响单线程执行结果的情况下,进行指令重排序,以期提高并行度。
有上述可以指令重排在单线程情况下,对程序的执行不会产生影响,但在多线程情况下就不一定了。所以上述过程的执行顺序可能发生变化,进而导致程序并不会按照预想的执行。
为解决上述问题以及保证并发编程的正确性,java中定义了 **happens-before**原则。在 《JSR-133:Java Memory Model and Thread Specification》 书中关于happens-before定义是这样的:
1.如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2.两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。
在Java中 为 避免指令重排出现,引入了volatile 关键字。正如你所看到那样在实例对象前就能保证执行结果的正确性。当一个线程调用getInstance()方法时,执行到synchronized关键字时就会上锁,其他线程也调用时就会发生阻塞,当然这种阻塞不是锁住整个类,而是仅仅锁住了方法。如过方法中的逻辑不是太复杂的话,对于外界来说是感知不到的。
这种方法终归还是要加锁的,只要加锁就会对程序性能产生影响。有什么解决办法可以实现不加锁,又能保证线程安全呢?
内部类:是指 一个类定义在另一个类里面或者一个方法里面 的类。有以下特点:
- 隐藏机制:内部封装性好,即便是同一个包下的类也不能直接访问
- 内部类可以访问外围类的私有数据
- 内部类对象可以不依赖外部实例被实例化
静态内部类:顾名思义 就是在内部类上加个static关键字 ,其特点有:
- 可以访问外部类静态成员
- 可以定义静态成员,非静态内部类不可以
静态内部类在载入Java的时候默认不加载,只有调用时进行加载。根据此特点双锁检查机制的单例模式可以改进使用静态内部类。
使用静态内部类
代码示例
public class Singleton{
private Singleton() {}
public static Singleton getInstance() {
return SingletonIner.instance;
}
//static是为了单例内存共享,保证这个方法不会被重写,重载
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}
上述方法及解决了饿汉式的内存浪费问题,又解决了懒汉式的锁的性能问题。
进一步思考
反射破坏单例
大家都知道在Java的各个框架中因为要实现某种功能,不可避免的使用到反射。反射有破坏封装性和性能低下的问题。在这里不考虑性能,只考虑封装性被破坏的问题。调用者使用反射,破坏了封装性,进而使实例有可能不止一个,这样就违背了使用单例模式的初衷。
如何解决呢?很简单,就是在创建另外的对象抛出异常,警告调用者,使其按照我们预想的方式进行调用。
代码示例
public class Singleton{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}
上面代码可以使调用者按照我们的想法使用。
序列化破坏单例
在实际应用中,为保存对象到磁盘或其他的存储介质,不可避免的要使用序列化。一个单例创建好之后,将其序列化保存在磁盘上,下次使用时在反序列化取出放到内存中使用。反序列化后的对象会重新分配内存,即重新创建,这样就违反了单例模式的初衷。以使用静态内部类的代码为我们单例模式类,下面进行简单测试。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) {
Singleton s1=null;
Singleton s2 = Singleton.getInstance();
FileOutputStream fos = null;
try {
fos=new FileOutputStream("singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis =new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton)ois.readObject();
ois.close();
System.out.println(s1==s2)
}
catch(Exception e){
e.printStackTrace();
}
}
}
上面代码运行后发现,输出竟然时false,这就说明反序列化后和序列话前的对象不是同一个,实例化了两次,根本不符合单例模式的原则。
如何改进呢? 改进 的方法也很简单就是增加readResolve()方法就可以。下面看代码
import java.io.Serializable;
public class Singleton implements Serializable{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
private Object readResolve() {
return SingletonIner.instance;
}
}
深究一下,为什么会这样呢?下面我们来看看ObjectInputStream里的readObject()方法一探究竟。代码如下:
/**
* Read an object from the ObjectInputStream. The class of the object, the
* signature of the class, and the values of the non-transient and
* non-static fields of the class and all of its supertypes are read.
* Default deserializing for a class can be overridden using the writeObject
* and readObject methods. Objects referenced by this object are read
* transitively so that a complete equivalent graph of objects is
* reconstructed by readObject.
*
* <p>The root object is completely restored when all of its fields and the
* objects it references are completely restored. At this point the object
* validation callbacks are executed in order based on their registered
* priorities. The callbacks are registered by objects (in the readObject
* special methods) as they are individually restored.
*
* <p>Exceptions are thrown for problems with the InputStream and for
* classes that should not be deserialized. All exceptions are fatal to
* the InputStream and leave it in an indeterminate state; it is up to the
* caller to ignore or recover the stream state.
*
* @throws ClassNotFoundException Class of a serialized object cannot be
* found.
* @throws InvalidClassException Something is wrong with a class used by
* serialization.
* @throws StreamCorruptedException Control information in the
* stream is inconsistent.
* @throws OptionalDataException Primitive data was found in the
* stream instead of objects.
* @throws IOException Any of the usual Input/Output related exceptions.
*/
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
根据注释,我们知道readObject()方法读取一个对象的类,类的签名以及该类机器所有超类的非瞬时和非静态的值。我们看到在try后面又调用了重写的readObject0()方法,其代码如下:
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
.......
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
.......
}
因篇幅的问题我省略了不重要的代码。
由上面看到,在TC_OBJECT处又调用了readOrdinaryObject()方法,其源码如下:
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
由上述代码可知,由调用了ObjectStreamClass的isInstanctiable()方法,方法体非常简单,源码如下 :
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
其作用就是构造方法是否为空,构造方法不为空就返回true。这意味着只要时无参构造方法就会实例化。
再回去看readOrdinaryObject()的源码。先是判断readResloveMethod是否为空,通过全局查找可知在私有方法ObjectStreamClass()给其赋值,赋值代码如下:
readResolveMethod = gerInheritableMethod(c1,"readResolve",null,Object.class);
之后上述的逻辑找到一个readResolve()方法如果存在就调用invokeReadResolve()方法,其代码如下:
/**
* Invokes the readResolve method of the represented serializable class and
* returns the result. Throws UnsupportedOperationException if this class
* descriptor is not associated with a class, or if the class is
* non-serializable or does not define readResolve.
*/
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
由invokeReadResource()方法又使用反射调用readResolveMethod(),进而执行readResolve()方法。
通过分析源码可以看出,readResolve()方法虽然解决了单例模式被破坏的问题,但是其实例化两次,只不过新创建的对象被覆盖了而已 。如果创建的对象动作发生加快,就意味着内存开销也随之增大。这个问题如何解决呢?使用注册式单例即可完美解决上诉问题。
注册式单例
枚举式单例
代码示例
public enum EnumSingleton{
INSTANCE;
private Object data;
/**
* @return Object return the data
*/
public Object getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
经过反编译分析源码可知枚举式单例是在静态代码块中为INSTANCE赋值,使饿汉式单例的体现。
那么序列化和反序列化能否破坏吗枚举式单例呢? 答案是不能。同查看源码可知枚举类型是通过类名和对象名找到全局唯一的对象。所以,枚举对象不可能加载多次。
那么反射呢?答案也是不能。在程序运行时会报java.lang.NoSuchMethodException
异常,其意思为没有找到无参的构造方法。查看java.lang.Enum
源码可知枚举类型只有一个protect
构造方法。经过测试,使用反射直接实例化枚举对象时会出现Cannot reflectively create objects
查看Constructor
的newInstsnce()
方法可知,在方法体做了判断,如果是枚举类型则直接抛出异常。
看到这个词,有的小伙伴的心里就想什么是容器式单例。容器式单例就是在单例类中维护一个类似与Map的容器,这种方式在Spring中是非常常见的,众所周知,Spring的Bean是全局单例的;Spring在内部维护着一个Map结构。在org.springframework.beans.factory.support
包下SimpleBeanDefinitionRegistry
为我们完美的解释容器式单例,其源码如下:
public class SimpleBeanDefinitionRegistry extends SimpleAliasRegistry implements BeanDefinitionRegistry {
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "'beanName' must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
this.beanDefinitionMap.put(beanName, beanDefinition);
}
@Override
public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
if (this.beanDefinitionMap.remove(beanName) == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
}
@Override
public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
BeanDefinition bd = this.beanDefinitionMap.get(beanName);
if (bd == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
return bd;
}
@Override
public boolean containsBeanDefinition(String beanName) {
return this.beanDefinitionMap.containsKey(beanName);
}
@Override
public String[] getBeanDefinitionNames() {
return StringUtils.toStringArray(this.beanDefinitionMap.keySet());
}
@Override
public int getBeanDefinitionCount() {
return this.beanDefinitionMap.size();
}
@Override
public boolean isBeanNameInUse(String beanName) {
return isAlias(beanName) || containsBeanDefinition(beanName);
}
}
其中BeanDefinition是一个接口,储存着各个单例对象的信息,由其实现类实现。对象名作为Map的Key,BeanDefinition作为Map的值,维护着这个map 保证每个对象全局单例.
因为Spring比较复杂,讨论暂告一段落。下面会到我们的主题,那我们的singleton
类如何实现容器式单例呢。下面看代码:
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
public class Singleton {
private static Map <String,Object > ioc =new ConcurrentHashMap();
private Singleton() {}
public static Object getInstance(String name) {
synchronized(ioc) {
if (!ioc.containsKey(name)){
Object o=null;
try {
o=Class.forName(name).newInstance();
ioc.put(name, o);
}catch(Exception e) {
e.printStackTrace();
}
return o;
}
else {
return ioc.get(name);
}
}
}
}
容器式单例适用于单例实例对象比较多的情况下,方便管理。值得注意的是,他是线程不安全的。
注册式单例就包括上面两种形式,每个都有不同的应用场景以及特点,要根据实际情况灵活选择。
下面我来介绍一种特殊的单例模式-----拥有ThreadLocal单例模式。
扩展
ThreadLocal与单例模式
话不多说,直接看代码。
public class Singleton {
private static final ThreadLocal<Singleton> instance = new ThreadLocal<> (){
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
private Singleton() {}
public static Object getInstance() {
return instance.get();
}
}
为什么说他特殊呢?因为加了ThreadLocal
关键字的单例类是线程内单例的,单线程共享不是单例的。大家可以测试下,使用下面的测试代码。
public class Main{
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Singleton.getInstance());
}
} ;
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
System.out.println("end");
}
}
执行结果如下:
测试后发现主线程无论执行多少次,获取的实例都是同一个,而两个子线程却获得了不同的实例。
声明
本文章为作者原创,其中参考了《spring5核心原理与30个类手写实战》以及互联网上的内容。如要转载请注明来源。
如有错误,请评论或者私聊我,欢迎探讨技术问题
spring源码学习之设计模式(1)单例模式的更多相关文章
- spring源码学习之路---深入AOP(终)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 上一章和各位一起看了一下sp ...
- spring源码学习之路---IOC初探(二)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 上一章当中我没有提及具体的搭 ...
- Spring源码学习
Spring源码学习--ClassPathXmlApplicationContext(一) spring源码学习--FileSystemXmlApplicationContext(二) spring源 ...
- Spring源码学习-容器BeanFactory(四) BeanDefinition的创建-自定义标签的解析.md
写在前面 上文Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签对Spring默认标签的解析做了详解,在xml元素的解析中,Spri ...
- Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签
写在前面 上文Spring源码学习-容器BeanFactory(二) BeanDefinition的创建-解析前BeanDefinition的前置操作中Spring对XML解析后创建了对应的Docum ...
- Spring源码学习-容器BeanFactory(二) BeanDefinition的创建-解析前BeanDefinition的前置操作
写在前面 上文 Spring源码学习-容器BeanFactory(一) BeanDefinition的创建-解析资源文件主要讲Spring容器创建时通过XmlBeanDefinitionReader读 ...
- Spring源码学习-容器BeanFactory(一) BeanDefinition的创建-解析资源文件
写在前面 从大四实习至今已一年有余,作为一个程序员,一直没有用心去记录自己工作中遇到的问题,甚是惭愧,打算从今日起开始养成写博客的习惯.作为一名java开发人员,Spring是永远绕不过的话题,它的设 ...
- 【目录】Spring 源码学习
[目录]Spring 源码学习 jwfy 关注 2018.01.31 19:57* 字数 896 阅读 152评论 0喜欢 9 用来记录自己学习spring源码的一些心得和体会以及相关功能的实现原理, ...
- Spring 源码学习——Aop
Spring 源码学习--Aop 什么是 AOP 以下是百度百科的解释:AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程通过预编译的方式和运行期动态代理实 ...
随机推荐
- [quartusⅡ] 使用quartusⅡ的过程中,遇到过的一些“软件上的问题”
1.USB blaster的驱动在设备管理器上点“更新驱动软件”,更新不了,说什么哈希值不在指定目录下,如下图, 解决方法是,https://blog.csdn.net/rdgfdd/article/ ...
- nyoj 811-变态最大值 (max)
811-变态最大值 内存限制:64MB 时间限制:1000ms 特判: No 通过数:6 提交数:15 难度:1 题目描述: Yougth讲课的时候考察了一下求三个数最大值这个问题,没想到大家掌握的这 ...
- 力扣(LeetCode)寻找数组的中心索引 个人题解
给定一个整数类型的数组 nums,请编写一个能够返回数组“中心索引”的方法. 我们是这样定义数组中心索引的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和. 如果数组不存在中心索引,那么我 ...
- UML:类图关系总结
UML类图几种关系的总结,泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖在UML类图中,常见的有以下几种关系: 泛化(Generalization), 实现(Reali ...
- golang学习--go中'继承'和多态
golang中没有继承的概念,这个struct属性上的继承,但是可以用匿名字段来模拟这个过程,方法上面的继承要使用接口.多态可以通过接口实现.可以看一下代码. package main import ...
- C++学习第二天(打卡)
C++ new 可以很方便的 分配一段内存. 比如 int *test= new int ; int n; cin>>n; int * test =new int [n]; 可以实现动态分 ...
- DDCTF2019 的四道题wp
MIsc:流量分析 这道题,在比赛的时候就差个key了,但是没想到要改高度,后来群里师傅说了下,就再试试, 导出来改高度. 导出来,把 把%5c(4)前面的hex删掉,改成png,就直接拿去那个img ...
- [FPGA]Verilog利用PWM调制巧妙完成RGB三色彩虹呼吸灯(给简约的题目以美妙的解答)
概述 实现彩虹呼吸灯 题目就是这么简短,但这是目前我碰到的最有意思的一道题目,因为他有无数种解决方法,并且每一种都是那么高级或者巧妙,比如 可以利用3路不同初相的PWM调制信号驱动三颗RGB灯重叠呼吸 ...
- /proc/cpuinfo文件解读(超易理解)
在linux系统中,提供了/proc目录下文件,显示系统的软硬件信息.如果想了解系统中CPU的提供商和相关配置信息,则可以查/proc/cpuinfo.但是此文件输出项较多,不易理解.例如我们想获取, ...
- 前端vue实现pdf文件的在线预览
3.前端vue实现pdf文件的在线预览 我是通过 <iframe> 标签就可以满足我工作的 pdf预览需求 如果<iframe> 无法满足需求 , 可以使用pdf.js这个插件 ...