38 Spring

news2024/11/25 20:33:37

38 Spring

参考资料

  1. Spring-全面详解(学习总结)

基本概念

Spring理念 : 使现有技术更加实用 . 本身就是一个大杂烩 , 整合现有的框架技术。

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架)。

IOC本质

IOC全称:控制反转**(Inversion of Control)**,是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI只是IoC的另一种说法。没有IoC的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。

举例来说,现在有一个接口UserDao和一个实现类UserDaoImpl,有一个UserServiceImpl接口如下:

public class UserServiceImpl implements UserService {
   private UserDao userDao = new UserDaoMySqlImpl();

   @Override
   public void getUser() {
       userDao.getUser();
  }
}

现在如果private UserDao userDao = new UserDaoMySqlImpl();需要更改成其他的对象时,我们就需要在这个类中进行更改,如果我们这样更改就会降低耦合:

public class UserServiceImpl implements UserService {
   private UserDao userDao;
   // 利用set实现
   public void setUserDao(UserDao userDao) {
       this.userDao = userDao;
  }

   @Override
   public void getUser() {
       userDao.getUser();
  }
}

传统开发中,需要调用对象的时候,需要调用者手动来创建被调用者的实例,即对象是由调用者new出来的。但是在Spring框架中,创建对象的工作不再由调用者来完成,而是交给IOC容器来创建,再推送给调用者,整个流程完成反转,所以是控制反转。

Bean的生命周期

这个概念在面试中经常被面试官询问,也就是说明这个Bean在Spring中的生生死死非常重要,而且了解Bean是如何创建和如何销毁的,对于我们理解Spring来说也是非常有益。那么接下来我们进入Spring的世界来看看这个世界最重要的Bean的一生。

先看一张图,这就是Bean的一生。

在这里插入图片描述

我们初看不明觉厉,我们接着往下走。

我们在了解一个人的一生时,会从他什么时候出生,什么时候接收了教育,什么时候去世来大致了解他的一生。对于Bean来说也是一样,我们只需要关注整体,不需要过多纠结细节,纠结过多的细节会让我们对Bean的了解模糊。

实例化(出生啦)

在Spring容器也就是IoC容器启动之后,就会到处寻找需要出生的Bean。这时候的Bean就是一个刚刚降临这个世界的婴儿,它在这个世界有了它的位置,但是它的身份和它长大工作需要的还没有准备就绪。

分配地址内存空间。

设置对象属性(出生证明)

一个人出生之后,需要给他起名字、赋予身份(身份证号码),以及其他重要信息(比如家庭背景)。这个阶段就像人的基础身份信息的建立

在实例化之后,Spring会对Bean进行属性赋值。这一步包括依赖注入,把Bean需要的资源和依赖(比如其他Bean、数据库连接等)赋值给它。

对属性进行赋值。

初始化(接收教育,准备工作)

这一步在正式步入职场之前,需要准备一些技能,这一步也就是初始化。初始化分很多步,这也正常,我们还得读小学、中学、大学呢。

我们来一步步看看其中的流程。

检查Aware相关接口并设置相关依赖

Aware接口的作用是让Bean感知Spring容器的某些特性,并在必要时获取Spring底层组件的引用。这种机制非常灵活,可以帮助开发者在需要时更好地与Spring环境交互,但要注意过度使用这些接口可能会增加代码的耦合性。

/**
 * @FileName MyBeanApplicationContextAware
 * @Description
 * @Author yaoHui
 * @date 2024-10-13
 **/
@Slf4j
@Component
public class MyBeanApplicationContextAware implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        log.error("ApplicationContextAware is running");
    }

    public void displayUserServiceBean(){
        UserService userService = applicationContext.getBean(UserService.class);
        userService.testApplicationContextAware();
    }
}
BeanPostProcessor前置处理

BeanPostProcessor 允许我们在Spring管理的Bean的生命周期中,插入自定义逻辑,这种机制使得我们可以非常灵活地管理和扩展Bean的行为。

常见的应用场景包括日志记录、动态代理、以及某些Bean的特殊处理。

在我们SSIC项目中,其中自定义的HuiMQ中,消费者会需要请求HuiMQ得到消息,需要得到消息的方法会被@HuiListener注解标注。而我们需要知道当前所有Bean中哪些方法是被该注解标记的,被该注解标记的需要加入一个带接受消息的方法集合中,后续将会通过反射来执行该方法。

这就是代码。

/**
 * @FileName HuiListenerAnnotationBeanPostProcessor
 * @Description
 * @Author yaoHui
 * @date 2024-10-12
 **/
@Component
public class HuiListenerAnnotationBeanPostProcessor implements BeanPostProcessor{

    private static final HuiListenerRegistry huiListenerRegistry = new HuiListenerRegistry();

    public static boolean huiListenerFlag = false;

    /***
    * @Description 在 bean 的初始化方法(如 @PostConstruct 注解的方法或 init-method 指定的方法)之前调用。
    * @param bean
    * @param beanName
    * @return {@link Object }
    * @Author yaoHui
    * @Date 2024/10/12
    */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /***
     * @Description 在 bean 的初始化方法之后调用。查看当前的bean是否存在被HuiListener注解过的方法
     * @param bean
     * @param beanName
     * @return {@link Object }
     * @Author yaoHui
     * @Date 2024/10/12
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        Method[] methods = bean.getClass().getMethods();
        for(Method method : methods){
            if(method.isAnnotationPresent(HuiListener.class)){
                processHuiListener(method,bean);
                huiListenerFlag = true;
            }
        }

        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }

    private void processHuiListener(Method method,Object bean){
        HuiListener huiListener = method.getAnnotation(HuiListener.class);
        HuiListenerEndpoint huiListenerEndpoint = new HuiListenerEndpoint();
        huiListenerEndpoint.setBean(bean);
        huiListenerEndpoint.setMethod(method);
        huiListenerRegistry.registerListenerEndpoint(huiListener.queueName(),huiListenerEndpoint);
    }

是否实现InitializingBean接口

InitializingBean接口的afterPropertiesSet()方法只会在实现该接口的特定Bean类的实例中执行一次。对于其他没有实现InitializingBean接口的Bean,这个方法不会被调用。

其会在Bean初始化之后执行,在所有参数赋值之后实现了这个接口的类就会执行。

/**
 * @FileName MyBeanPostProcessor
 * @Description
 * @Author yaoHui
 * @date 2024-10-13
 **/
@Component
@Slf4j
public class MyBeanPostProcessor implements BeanPostProcessor, InitializingBean {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//        log.error("初始化之前执行:" + bean.toString());
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//        log.error("初始化之后执行:" + bean.toString());
        return bean;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.error("afterPropertiesSet is running");
    }
}
BeanPostProcessor后置处理
/**
 * @FileName HuiListenerAnnotationBeanPostProcessor
 * @Description
 * @Author yaoHui
 * @date 2024-10-12
 **/
@Component
public class HuiListenerAnnotationBeanPostProcessor implements BeanPostProcessor{

    private static final HuiListenerRegistry huiListenerRegistry = new HuiListenerRegistry();

    public static boolean huiListenerFlag = false;

    /***
    * @Description 在 bean 的初始化方法(如 @PostConstruct 注解的方法或 init-method 指定的方法)之前调用。
    * @param bean
    * @param beanName
    * @return {@link Object }
    * @Author yaoHui
    * @Date 2024/10/12
    */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /***
     * @Description 在 bean 的初始化方法之后调用。查看当前的bean是否存在被HuiListener注解过的方法
     * @param bean
     * @param beanName
     * @return {@link Object }
     * @Author yaoHui
     * @Date 2024/10/12
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        Method[] methods = bean.getClass().getMethods();
        for(Method method : methods){
            if(method.isAnnotationPresent(HuiListener.class)){
                processHuiListener(method,bean);
                huiListenerFlag = true;
            }
        }

        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }

    private void processHuiListener(Method method,Object bean){
        HuiListener huiListener = method.getAnnotation(HuiListener.class);
        HuiListenerEndpoint huiListenerEndpoint = new HuiListenerEndpoint();
        huiListenerEndpoint.setBean(bean);
        huiListenerEndpoint.setMethod(method);
        huiListenerRegistry.registerListenerEndpoint(huiListener.queueName(),huiListenerEndpoint);
    }
注册Destruction相关接口

DisposableBean接口提供了一个destroy()方法,当Bean被销毁时,Spring会调用该方法。

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component
public class MyBean implements DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        // 自定义的销毁逻辑
        System.out.println("MyBean is being destroyed!");
    }
}

这里并不是真正的销毁,Bean还没开始使用呢,这只是定义了一个方法,方便之后进行销毁时进行调用该方法。

使用@PreDestroy注解

Spring还允许使用@PreDestroy注解来标记一个方法,在Bean被销毁之前调用。示例代码如下:

java复制代码import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class AnotherBean {
    
    @PreDestroy
    public void cleanup() {
        // 自定义的清理逻辑
        System.out.println("AnotherBean is being destroyed!");
    }
}
使用

不必多说

销毁
是否实现了DisposableBean接口

DisposableBean 是 Spring 框架中的一个接口,专门用于处理 Bean 的销毁过程。通过实现该接口,开发者可以在 Bean 的生命周期结束时执行自定义的清理逻辑,以确保资源的正确释放。

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component
public class MyBean implements DisposableBean {

    // 这里可以定义一些需要的资源,比如数据库连接等
    private String resource;

    public MyBean() {
        // 模拟资源的初始化
        this.resource = "Some Resource";
        System.out.println("MyBean initialized with resource: " + resource);
    }

    // 实现 DisposableBean 接口的 destroy 方法
    @Override
    public void destroy() throws Exception {
        // 自定义的清理逻辑
        System.out.println("MyBean is being destroyed! Releasing resource: " + resource);
        // 这里可以添加资源释放的代码,比如关闭数据库连接等
    }
}

是否配置自定义的destory-method

destroy-method 是 Spring 框架中用于定义 Bean 销毁方法的一个属性,主要用于在 Bean 被销毁时指定一个特定的方法来执行清理逻辑。这种方式通常用于 XML 配置文件中,允许开发者在 Spring 容器关闭或 Bean 被销毁时执行自定义的逻辑。

Bean的作用域

Bean的作用域是指Bean实例的生命周期及可见性范围,Spring框架定义了以下6种作用域:

  • singleton:单例作用域,所有对该Bean的请求都返回同一个Bean实例。
  • prototype:原型作用域,每次请求时都创建一个新的Bean实例。
  • request:请求作用域,每个HTTP请求都会创建一个新的Bean实例,该Bean实例仅在当前请求内有效。
  • session:会话作用域,每个HTTP会话都会创建一个新的Bean实例,该Bean实例仅在当前会话内有效。
  • application:全局作用域,一个bean 定义对应于单个ServletContext 的生命周期。
  • websocket: HTTP WebSocket 作用域,一个bean 定义对应于单个websocket 的生命周期。

singleton作用域是Spring中默认的作用域,

使⽤ @Scope 标签就可以⽤来声明 Bean 的作⽤域,⽐如设置 Bean 的作⽤域,如下代码所示:

@Component
public class Users {
    @Scope(prototype)
    @Bean(name = "u")
    public User user() {
        User user = new User();
        user.setId(1);
        user.setName("Hi user"); 
        return user;
    }
}

Spring依赖注入的方式

构造器注入
import org.springframework.stereotype.Component;

@Component
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void addUser(String username) {
        userRepository.save(username);
    }
}

@Component
public class UserRepository {
    public void save(String username) {
        System.out.println("User " + username + " saved.");
    }
}

Setter 方法注入
import org.springframework.stereotype.Component;

@Component
public class OrderService {
    private PaymentService paymentService;

    // Setter 方法
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void processOrder() {
        if (paymentService != null) {
            paymentService.processPayment();
        } else {
            System.out.println("No payment service available.");
        }
    }
}

@Component
public class PaymentService {
    public void processPayment() {
        System.out.println("Payment processed.");
    }
}

字段注入
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NotificationService {
    
    @Autowired
    private EmailService emailService;

    public void sendNotification() {
        emailService.sendEmail();
    }
}

@Component
public class EmailService {
    public void sendEmail() {
        System.out.println("Email sent.");
    }
}

注解配置方式
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppConfig {

    @Value("${app.name}")
    private String appName;

    public void printAppName() {
        System.out.println("Application Name: " + appName);
    }
}

XML 配置
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userRepository" class="com.example.UserRepository"/>
    
    <bean id="userService" class="com.example.UserService">
        <constructor-arg ref="userRepository"/>
    </bean>
</beans>

BeanFactory和ApplicationContext有什么区别

是spring的核心接口,都可以作为容器,ApplicationContext是BeanFactory的子接口。
BeanFactory: 是spring最底层的接口,包含各种Bean的定义和Bean的管理。

区别:

  • BeanFactroy采用的是延迟加载形式来注入Bean的,使用到bean才会加载。ApplicationContext一次性加载所有bean。
  • BeanFactory需要手动注册,而ApplicationContext则是自动注册。
  • BeanFactory不支持国际化,ApplicationContext支持国际化(实现MessageSource接口)。
  • BeanFactory不支持AOP,ApplicationContext支持AOP,可以与Spring的AOP框架集成,提供声明式事务管理。

Spring中的单例bean的线程安全问题

虽然Spring中的Bean是singleton,但是在一些多线程环境下,会出现线程安全的问题。比如下面这种情况:

import org.springframework.stereotype.Component;

@Component
public class CounterService {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}



import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {
    
    @Autowired
    private CounterService counterService;

    @Override
    public void run(String... args) throws Exception {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counterService.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counterService.increment();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counterService.getCount());
    }
}

因为count++;操作不是原子性的,所以会出现线程不安全的问题。一般的解决方法是:

  • 所定义的Bean是无状态的,即不涉及一些数据,但是这不现实;
  • 在类中定义一个ThreadLocal成员变量,将需要的可变变量保存在ThreadLocal中;
  • 把成员变量写在方法内。
  • 修改bean的作用域,singleton改为prototype。(@Scope(“prototype”))
  • 使用synchronized修饰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2212016.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Redis】分布式(day12)

引入 在实际生产中&#xff0c;只部署一个Redis时&#xff0c;就会造成单点问题&#xff1a; 可用性问题&#xff0c;单个节点部署的Redis&#xff0c;如果此时该服务器挂了&#xff0c;那么意味着Redis整体的服务也就断掉了。性能/并发也是比较有限的。 为了避免单点问题的…

如何在UE5中创建加载屏幕(开场动画)?

第一步&#xff1a; 首先在虚幻商城安装好Async Loading Screen&#xff0c;并且在项目的插件中勾选好。 第二步&#xff1a; 确保准备好所需要的素材&#xff1a; 1&#xff09;开头的动画视频 2&#xff09;关卡加载图片 3&#xff09;准备至少两个关卡 第三步&#xff1a…

通信工程学习:什么是SPI串行外设接口

SPI&#xff1a;串行外设接口 SPI&#xff0c;即串行外设接口&#xff08;Serial Peripheral Interface&#xff09;&#xff0c;是一种由Motorola公司首先在其MC68HCXX系列处理器上定义的同步串行接口技术。SPI接口主要用于微控制器&#xff08;MCU&#xff09;与外部设备之间…

spring |Spring Security安全框架 —— 认证流程实现

文章目录 开头简介环境搭建入门使用1、认证1、实体类2、Controller层3、Service层3.1、接口3.2、实现类3.3、实现类&#xff1a;UserDetailsServiceImpl 4、Mapper层3、自定义token认证filter 注意事项小结 开头 Spring Security 官方网址&#xff1a;Spring Security官网 开…

leetcode 1027 最长等差数列 题目的思考

https://leetcode.cn/problems/longest-arithmetic-subsequence/ 如果序列是&#xff1a;3 0 3&#xff0c;枚举的公差是3 对于第一个数3&#xff0c;它的序列长度就是他自己3 对于第二个数0&#xff0c;它的序列长度就行它自己0 对于第三个数&#xff0c;它的序列长度应该是【…

【未知列名注入】

简介 在sql注入中&#xff0c;如果服务器过滤了column_name阻止我们获取列名&#xff0c;我们该如何绕过 一、union 绕过 使用union构造多个表&#xff0c;把数据表和构造的123表连接起来&#xff0c;我们看一下构造过程: 查询user表数据 select * from user;Union联合查询…

基于协同过滤的景区旅游可视化与景区推荐系统(自动爬虫,地点可换)

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍过程展示项目移植每文一语 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主 项目介绍 本项目是一个综合性的旅游景区数据管理与分析推荐系统,集成了用…

Qt:设置程序图标与主窗口背景图片

目录 设置程序图标&#xff1a; 设置主窗口背景图片&#xff1a; 设置程序图标&#xff1a; 在设置图标之前先准备一张ico图标&#xff0c;没有ico图标的可以准备一张图片&#xff0c;然后找一个在线的ico转换网站去转换一张ico文件出来。 然后打开项目文件所在的文件夹&am…

C语言 | Leetcode C语言题解之第467题环绕字符串中唯一的子字符串

题目&#xff1a; 题解&#xff1a; #define MAX(a, b) ((a) > (b) ? (a) : (b))int findSubstringInWraproundString(char * p) {int dp[26];int len strlen(p);memset(dp, 0, sizeof(dp));int k 0;for (int i 0; i < len; i) {if (i && (p[i] - p[i - 1] …

Spark高级用法-数据源的读取与写入

目录 数据读取 数据写入 总结 数据读取 读文件 read.json read.csv csv文件有两个部分构成 头部数据&#xff0c;也就是字段数据&#xff0c;行数数据 read.orc 读数据库 read.jdbc(jdbc连接地址,table表名,properties{user用户名,password密码,driver驱动信息}) 缺少连…

机器学习基础概念(3)

小小考一下大家前两节的内容(坏笑) 我们如何评判一个机器学习模型的性能呢&#xff1f; 通常是判断它的泛化能力&#xff08;对于未知数据的处理能力&#xff09; 那么对于泛化能力是否有一个标准&#xff0c;比如在未知的1万个数据中&#xff0c;泛化能力 模型一90% >…

【分布式事务-02】分布式事务seata的安装下载与环境搭建

redis系列整体栏目 内容链接地址【一】分布式事务之2pc两阶段提交https://zhenghuisheng.blog.csdn.net/article/details/142406325【一】分布式事务seata的安装下载与环境搭建https://zhenghuisheng.blog.csdn.net/article/details/142893117 分布式事务seata的安装下载与环境…

java服务器技术

1. Java EE&#xff08;Java Enterprise Edition&#xff09; Java EE是一套为企业级应用提供的完整解决方案&#xff0c;它包括了Java Servlet、JSP&#xff08;JavaServer Pages&#xff09;、EJB&#xff08;Enterprise JavaBeans&#xff09;、JPA&#xff08;Java Persist…

【风力发电】基于模糊逻辑控制的风电系统MPPT

摘要 本文基于模糊逻辑控制 (Fuzzy Logic Control, FLC) 实现了风力发电系统的最大功率点追踪 (MPPT)。FLC 由于其不依赖于精确数学模型的特点&#xff0c;能够有效应对风速变化导致的非线性和不确定性问题。通过对风速和功率的模糊化处理&#xff0c;该方法提高了风电系统的功…

ros1:使用C++编写ros程序,获取IMU数据,使用gazebo仿真

cd catkin_ws/src/catkin_create_pkg imu_pkg roscpp rospy sensor_msgs在src目录下创建&#xff0c;imu_node.cpp #include "ros/ros.h" #include "sensor_msgs/Imu.h" #include "tf/tf.h"void IMUCallback(sensor_msgs::Imu msg){if(msg.orien…

深兰科技|“武汉市AI心理热线医工交叉研发合作基地”正式揭牌

2024年10月10日是第33个世界精神卫生日&#xff0c;以“共建共治共享&#xff0c;同心健心安心“为主题的武汉市2024年世界精神卫生日主题活动暨第三届武汉青年心理情景剧展演闭幕式&#xff0c;在武汉隆重举行。期间&#xff0c;还举行了武汉市精神卫生中心与深兰科技(武汉)公…

Video-LLaMA部署

Video-LLaMA: An Instruction-tuned Audio-Visual Language Model for Video Understanding

计组_输入输出系统

2024.08.05&#xff1a;计算机组成原理输入输出学习笔记 第25节 输入输出系统 5.1 IO基本职能5.2 IO接口的通用结构5.3 IO数据传送控制方式5.3.1 程序直接控制&#xff08;程序查询控制&#xff09;&#xff08;1&#xff09;独占查询&#xff08;2&#xff09;定时查询 5.3.2 …

衡石分析平台---分析人员手册

分析人员是 HENGSHI SENSE 系统最主要的用户。在企业内部&#xff0c;他们应该是了解相关业务&#xff0c;需要在纷繁复杂的数据中发现数据规律的人。 从岗位上来说&#xff0c;他们可能来自运营部门&#xff0c;需要从日常运营数据中发现用户的使用规律&#xff1b;可能来自销…

【element-tiptap】如何引进系统中的字体?

源码地址&#xff1a; https://github.com/Leecason/element-tiptap 源码中给出的字体如下 可以看到&#xff0c;咱们日常需要的黑体、微软雅黑等都没有&#xff0c;所以这篇文章来探索一下怎么加字体。 另外呢&#xff0c;肯定有小伙伴发现&#xff0c;这个按钮点击的时候&am…