Sping学习笔记(附实例,超详细)

目录

1.先来谈谈什么是Spring

1.1编写Spring代码需要导入的依赖:(其中包含了很多jar包)

 2.控制反转(IOC)理论推导

2.1 在Test中获取Spring的上下文对象

2.2依赖注入

2.3 拓展:C标签和P标签注入

3.控制反转

4.IOC创建对象的方式

4.1 下标赋值,但需要创建有参构造器

4.2 通过类型创建 不建议使用

4.3 直接通过参数来设置

5.数据库连接池

6.Bean的作用域

6.1单例模式:(Spring默认机制)

6.2原型模式

6.3.其余的 request、session、application

7.Bean的自动装配

7.1使用注解实现自动装配

7.1.1@Autowired

7.1.2导入约束

7.1.3 Bean的实现

7.1.4 属性注入

7.2 配置注解支持

7.2.1@Autowired和@Resource的区别

7.3 XML与注解比较

8.代理模式

8.1 AOP是用来做什么的

8.2静态代理

8.3 深入理解静态代理

9.动态代理

9.1实例

9.2 深入了解动态代理

9.3 代理模式总结

10. 了解反射

11.AOP

11.1 什么是AOP

11.2 Aop在Spring中的作用 

 11.2.1Spring AOP 的两种代理方法

11.3 正题开始,首先导包

11.4具体分为三种方式

11.4.1第一种方式:使用原始的SpringAPI接口

11.4.2 方法二 自定义类来实现Aop

11.4.3 方式三 使用注解实现

11.5 AOP''心血''总结

12.声明式事务 

12.1 事务的四个属性

12.2 Spring中的事务管理 

12.2.1 事务管理器 

12.3 spring事务传播特性 

12.4 头文件的约束导入 

12.5 案例 

13.阶段性自我总结


1.先来谈谈什么是Spring


    1.Spirng是一个免费的开源的容器(框架);
    2.Spring是一个轻量级的,非入侵式的框架;
    3.控制反转(IOC),面向切面编程(AOP);
    4.支持事务的处理,对框架整合的支持!



1.1编写Spring代码需要导入的依赖:(其中包含了很多jar包)

地址:<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>6.0.2</version>
</dependency>

 
2.控制反转(IOC)理论推导

  • 从本质上解决了需要程序员去管理对象的创建系减少统的耦合性
  • 程序被动的提供接口,主动权在用户的手上,只需要获取到用户的操作,然后调用到对应的方法
  • 控制反转是一种通过描述(xml或注解)并通过第三方去生成或获取特定对象的方式。
  • 在Spring中实现控制反转的是IOC容器,其实现方法是依赖注入(DI)

2.1 在Test中获取Spring的上下文对象

ApplicationContext context = new ClassPathXmlApplicationContext("xml文件");

        然后需要谁就去.get谁

2.2依赖注入

实例:

public class Address {
    private String address;

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Address{" +
                "address='" + address + '\'' +
                '}';
    }
}
//对应不同的注入方式
public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbys;
    private Map<String,String> card;
    private Set<String> games;
    private String wife;
    private Properties info;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", address=" + address.toString() +
                ", books=" + Arrays.toString(books) +
                ", hobbys=" + hobbys +
                ", card=" + card +
                ", games=" + games +
                ", wife='" + wife + '\'' +
                ", info=" + info +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String[] getBooks() {
        return books;
    }

    public void setBooks(String[] books) {
        this.books = books;
    }

    public List<String> getHobbys() {
        return hobbys;
    }

    public void setHobbys(List<String> hobbys) {
        this.hobbys = hobbys;
    }

    public Map<String, String> getCard() {
        return card;
    }

    public void setCard(Map<String, String> card) {
        this.card = card;
    }

    public Set<String> getGames() {
        return games;
    }

    public void setGames(Set<String> games) {
        this.games = games;
    }

    public String getWife() {
        return wife;
    }

    public void setWife(String wife) {
        this.wife = wife;
    }

    public Properties getInfo() {
        return info;
    }

    public void setInfo(Properties info) {
        this.info = info;
    }
}

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
    <bean id="address" class="com.gcx.pojo.Address">
        <property name="address" value="青岛"/>
    </bean>
    <bean id="student" class="com.gcx.pojo.Student">
<!--        第一种普通值注入 value-->
        <property name="name" value="gcx"/>
<!--        第二种注入bean注入, ref-->
        <property name="address" ref="address"/>
<!--        数组注入-->
        <property name="books">
            <array>
                <value>红楼梦</value>
                <value>西游记</value>
                <value>水浒传</value>
                <value>三国演义</value>
            </array>
        </property>
<!--        List-->
        <property name="hobbys">
            <list>
                <value>听歌</value>
                <value>看电影</value>
                <value>玩游戏</value>
            </list>
        </property>
<!--        map-->
        <property name="card">
            <map>
                <entry key="身份证" value="111111111111111111"/>
                <entry key="银行卡" value="684546554466455545"/>
            </map>
        </property>
<!--        set-->
        <property name="games">
            <set>
                <value>LOL</value>
                <value>COC</value>
                <value>BOB</value>
            </set>
        </property>
<!--        null-->
        <property name="wife">
            <null/>
        </property>
<!--        Properties-->
        <property name="info">
            <props>
                <prop key="studyId">78945621</prop>
                <prop key="phone">20221206</prop>
                <prop key="姓名">小明</prop>
            </props>
        </property>
    </bean>


</beans>

 Test测试

public class Mytest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Student student = (Student) context.getBean("student");
        System.out.println(student.toString());
    }

2.3 拓展:C标签和P标签注入

public class User {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

UserBean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!--需要先导入约束(第4 5行)-->
    <!--    P命名空间注入-->
    <bean id="User" class="com.gcx.pojo.User" p:name="gcx" p:age="18"/>
<!--C命名空间注入 通过有参无参注入-->
    <bean id="user2" class="com.gcx.pojo.User" c:age="18" c:name="cxg"/>
</beans>

Test测试:

@Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("UserBean.xml");
        User user = (User) context.getBean("user2");//测试P标签对应beanid为user
        System.out.println(user);
    }


3.控制反转


    控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的; 使用Spring                     后,对象是由Spring来创建的.
    反转:程序本身不创建对象,而变成被动的接收对象。
    依赖注入:就是利用set方法来进行注入.
    IOC是一种编程思想,由主动的编程变成被动的接收.


要实现不同的操作,只需要在XML配置文件中进行修改,所谓的IOC就是对象由Spring来创建,管理,装配Bean! 

4.IOC创建对象的方式


使用无参构造器

<property name="name" value="123"/>


4.1 下标赋值,但需要创建有参构造器
 

<constructor-arg index="0" value="456"/>


4.2 通过类型创建 不建议使用

<constructor-arg type="java.lang.String" value="gcx"/>


4.3 直接通过参数来设置

<bean id="user" class="com.gcx.User">
        <constructor-arg name="name" value="gcx"/>
</bean>

5.数据库连接池


池化技术:准备一些预先的资源,过来就链接预先准备好的
最小连接数:根据常用的连接数设定
最大连接数:设置最大连接数,当连接数超过最大连接数是就要进行排队等待
等待超时:当等待时间超过排队等待时间时就自动退出等待,并提示出错

编写连接池,实现一个接口 DataSource
常用数据池:

  • DBCP(导入jar包)
  • c3p0(导入jar包)
  • Druid:阿里巴巴

使用这些数据库链接池后就不需要编写数据库的代码了;
无论使用什么数据源,本质都是一样的:Datasource接口不会变,方法就不会变

6.Bean的作用域


6.1单例模式:(Spring默认机制)

<!--<bean id="accountService" class="com.something.DefaultAccountService"/>
(singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>


6.2原型模式

缺点:每次从容器中get的时候,都会产生一个新的对象(浪费资源)!
 

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>


6.3.其余的 request、session、application

这些只能在web开发中使用到。
官网原话:The request, session, application, and websocket scopes are available only if you use a web-aware Spring ApplicationContext implementation (such as XmlWebApplicationContext). 
If you use these scopes with regular Spring IoC containers, 
such as the ClassPathXmlApplicationContext, 
an IllegalStateException that complains about an unknown bean scope is thrown.

7.Bean的自动装配


三种自动装配方式:

                                  1.在xml中显示配置
                                  2.在java中显示配置
                                  3.隐式的自动装配bean【重要】(byName,byType)


7.1使用注解实现自动装配

1.需要导入aop的jar包(查看maven插件)

2.@Autowired(常用)、@Resource

7.1.1@Autowired

实例:

public class Cat {
    public void shout(){
        System.out.println("miao");
    }
}
public class Dog {
    public void shout(){
        System.out.println("wang");
    }
}
public class People {
//    如果显示定义了Autowired属性的required为false,说明这个属性可以为null,否则不允许为空;
//    @Autowired(required = false)
    @Autowired
    @Qualifier(value = "cat11")
//    @Qualifier(value = "beanid")当自动装配无法通过一个注解完成时,用来指定一个唯一的bean对象注入
    private Cat cat;
    @Autowired
    @Qualifier(value = "dog11")
    private Dog dog;
    private String name;

    @Override
    public String toString() {
        return "People{" +
                "cat=" + cat +
                ", dog=" + dog +
                ", name='" + name + '\'' +
                '}';
    }

    public Cat getCat() {
        return cat;
    }

    public void setCat(Cat cat) {
        this.cat = cat;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 beans.xml(不规范)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <context:annotation-config/>
<!--    使用Autowired可以不用set方法,前提是自动装配的属性在IOC容器中存在且符合名字byName-->
        <bean id="cat" class="com.gcx.pojo.Cat"/>
        <bean id="cat11" class="com.gcx.pojo.Cat"/>
        <bean id="dog" class="com.gcx.pojo.Dog"/>
        <bean id="dog11" class="com.gcx.pojo.Dog"/>
<!--    自动装配:autowire    byName:会自动在容器上下文中查找,和自己对象set方法后面的值对应的beanid(必须对应相同,所有bean的id唯一)
                    byType:会自动在容器上下文中查找,和自己对象属性类型对应相同bean(必须保证所有bean的class全局唯一)-->
    <bean id="people" class="com.gcx.pojo.People">
        <property name="name" value="gcx"/>
<!--        <property name="cat" ref="cat"/>-->
<!--        <property name="dog" ref="dog"/>-->
     </bean>
<!--    开启注解支持-->

</beans>

Test测试

public class Mytest {
    @Test
    public void test(){
        ApplicationContext Context = new ClassPathXmlApplicationContext("beans.xml");
        People people = (People) Context.getBean("people");
        people.getDog().shout();
        people.getCat().shout();
    }
}


7.1.2导入约束

xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd


7.1.3 Bean的实现


指定注解扫描包,用来配置扫描哪些包下的注解

<context:component-scan base-package="com.gcx"/>


7.1.4 属性注入


    (1)可以不用提供set方法,直接在直接名上添加@value(“值”);
    (2)如果提供了set方法,在set方法上添加@value(“值”);


7.2 配置注解支持

<context:annotation-config/> //若使用注解就必须开启,否则注入为空


如果@Autowired注解自动装配的环境比较复杂,自动装配无法通过一个注解【@Autowired】完成的时候,我们可以使用@Qualifier(value = "xxx")去配置@Autowired的使用,指定一个唯一的bean对象注入!


7.2.1@Autowired和@Resource的区别


1.都是用来自动装配的,都可以放在属性字段上
2.@Autowired是通过byType的方法实现
3.@Resource是默认通过byName的方法,如果找不到相同的名字,则会通过byType实现!
如果两个都找不到的情况下就会报错!
4.执行顺序不同:@Autowired是通过byType的方法实现,@Resource是默认通过byName的方法实现。

7.3 XML与注解比较


1.XML可以适用任何场景 ,结构清晰,维护方便
2.注解不是自己提供的类使用不了,开发简单方便

xml与注解整合开发 :推荐最佳实践
    xml去管理Bean,让注解完成属性注入
    /**使用过程中,可以不用扫描,扫描是为了类上的注解*/

8.代理模式

该部分需要先理解什么是反射,明白什么是反射可以帮助理解什么是代理模式,具体看目录10

[AOP(面向切面编程(AOP))的底层机制就是动态代理!]


8.1 AOP是用来做什么的

在不改变原来的代码的情况下,实现了对原有功能的增强,这是AOP中最核心的思想;


代理模式分为:
        1.静态代理
        2.动态代理


静态代理角色分析
/**
    *    抽象角色 :  一般使用接口或者抽象类来实现(相当于一个方法或行为)
    *    真实角色 :  被代理的角色
    *    代理角色 :  代理真实角色 ; 在代理真实角色后 , 一般会有一些自带的操作 .
    *        客户 :     使用代理角色来进行一些操作
*/


静态代理的优缺点
/**
*优点:
*    1. 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
*    2. 公共的业务由代理来完成 . 实现了业务的分工 ,
*    3. 公共业务发生扩展时变得更加集中和方便 .
*缺点 :
*类(真实角色)多了 , 多了代理类(代理角色) , 需要写的代码翻倍,工作量变大了,开发效率降低

8.2静态代理

实例:可以用[真实角色:房东;  抽象角色(方法):租房子;  代理角色:中介;   客户:买房子的人]来描述整个静态代理

房东:

//房东
public class Host implements Rent{

    @Override
    public void rent() {
        System.out.println("房东出租房子");
    }
}

租房子:

//租房
public interface Rent {
    void rent();
}

中介:

//中介
//这里中介去实现了租房子这个方法,就相当于中介帮助房东出租房子
public class ZhongJie implements Rent{
//去找到房东,为了获取房源
    private Host host;

    public ZhongJie() {
    }
//为房东创建实参,为了实现出租房子的方法(行为)
    public ZhongJie(Host host) {
        this.host = host;
    }

    public void rent() {
//通过房东去.出来买房子的方法
        host.rent();
//这是中介(代理对象)自带的一些方法
        seeHouse();//看房子
        hetong();//签合同
        takeMoney();//赚差价
        sell();//卖出房子
    }
/**
*对应的中介自带的方法
*/
//    看房
    public void seeHouse(){
        System.out.println("中介带你看房");
    }
//签合同
    public void hetong(){
        System.out.println("中介签合同");
    }
//赚差价
    public void takeMoney(){
        System.out.println("中介收中介费");
    }
//卖出房子
    public void sell(){
        System.out.println("卖出房子");
    }
}

买房子的人:

public class Empty {
    public static void main(String[] args) {
//        房东要租房子
        Host host = new Host();
//        host.rent();
//        房东去找中介,中介帮房东租房子,但是代理角色一般会有一些自带的操作!
        ZhongJie zhongJie = new ZhongJie(host);
//        有了中介,租房子的人不需要面对房东,直接找中介即可
        zhongJie.rent();
    }
}

输出结果:

房东要出租房子
中介带你看房
中介签合同
中介收中介费
卖出房子

从上边的代码中可以很容易的看出来,房东只是提供了一个我要卖房子的方法(行为)以及房源;剩下的其他事情都是由代理商(中介)来提供的,直到把房子卖出去;

同样的如果需要什么其他的操作直接在中介中去实现即可.

 8.3 深入理解静态代理

在不改变原来的代码的情况下,实现了对原有功能的增强

 

实例

//先去写出一些方法
public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}
然后去用实现类实现接口内的方法
public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("增加了一个用户");
    }

    @Override
    public void delete() {
        System.out.println("删除了一个用户");
    }

    @Override
    public void update() {
        System.out.println("更改了一个用户");
    }

    @Override
    public void query() {
        System.out.println("查询了一个用户");
    }
}

此时,若直接写Test测试

public class Test {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        userService.add();
    }
}

它会出现的结果是

增加了一个用户

去调用其他的方法也是对应显示其中sout的内容,但是此时,我们亲爱的甲方提出了要求:

我需要在每一个原有输出的基础上,去新增一个输出,去告诉用户我们后台使用了那些方法;

 首先我们最直接的方法就是在实现类去再写一个sout去输出我们使用的方法,但是如果我的方法有100条呢?1000条呢?甚至更多怎么办? 总不能去一个个的添加吧,所以就需要使用到我们的AOP(面向切面编程)方法,就相当于去设置一个中间商,先去得到我们原有的方法,再去新增新的内容:

public class UserServiceDaiLi implements UserService{
//获取原有实现类方法
    private UserServiceImpl userservice;
//设置set方法
    public void setUserservice(UserServiceImpl userservice) {
        this.userservice = userservice;
    }
//因为在这里实现了UserService接口,所以去重写(可以不写重写注释,实质上也不算重写)原来内部的信息
    @Override
    public void add() {
        log("add");
        userservice.add();
    }

    @Override
    public void delete() {
        log("delete");
    userservice.delete();
    }

    @Override
    public void update() {
        log("update");
userservice.update();
    }

    @Override
    public void query() {
        log("query");
   userservice.query();
    }
//这里去写我们要新添加的内容,然后在上边的方法中去添加该方法即可达到目的
    public void log(String msg){
//        添加日志
        System.out.println("[Debug]使用了"+msg+"方法");
    }
}

Test测试:

public class Test {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        UserServiceDaiLi DaiLi = new UserServiceDaiLi();
        DaiLi.setUserservice(userService);
//因为要输出的是daili内部新写的内容
        DaiLi.add();

    }
}

输出为:

[Debug]使用了add方法
增加了一个用户

9.动态代理

/**优点:
*    1. 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情,
*    2. 公共的业务由代理来完成 . 实现了业务的分工 ,
*    3. 公共业务发生扩展时变得更加集中和方便 ,
*    4. 一个动态代理类代理的是一个接口,一般就是对应的一类业务,
*    5.一个动态代理类可以代理多个类,只要是实现了同一个接口即可. 
*/

----该部分较为抽象比较难理解,直接上代码,具体解释都写在代码里了

----首先还是以静态代理的数据为例

9.1实例

房东:

//房东
public class Host implements Rent {

    @Override
    public void rent() {
        System.out.println("房东要出租房子");
    }
}

租房子:

//租房
public interface Rent {
    void rent();
}

代理对象(官方为ProxyInvocationHandler,为了方便理解起名DaiLi~~~):

//使用这个类自动生成代理角色
public class DaiLiInvocationHandler implements InvocationHandler {

//    被代理的接口
    private Rent rent;

    public void setRent(Rent rent) {
        this.rent = rent;
    }

//    生成得到代理类

    /**
     * @return
     * getClassLoader() 是为了加载类在哪个位置
     * getInterfaces() 表示要代理的接口是哪一个
     * this 代表InvocationHandler本身
     */
     public Object getProxy(){
       return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);
     }

    //(真正的处理是靠invoke方法执行)处理代理实例,并返回结果
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//        动态代理的本质就是使用反射机制实现
        seeHouse();
        Object result = method.invoke(rent,args);
        takeMoney();
        return result;
    }
    public void seeHouse(){
        System.out.println("中介带去看房子");
     }
     public void takeMoney(){
        System.out.println("中介收差价");
     }

}

卖房子的人:

public class Client {
    public static void main(String[] args) {
//        真实角色
        Host host = new Host();
//        代理角色(暂时不存在,需要去调用set设置)
        DaiLiInvocationHandler dih = new DaiLiInvocationHandler();
//        通过调用程序处理角色,来处理我们调用的接口对象!
        dih.setRent(host);
        Rent proxy = (Rent) dih.getProxy();
        proxy.rent();
    }
}

输出结果:

中介带去看房子
房东要出租房子
中介收差价

9.2 深入了解动态代理

继8.3来深入了解动态代理

代理对象:

//使用这个类自动生成代理角色
public class DaiLiInvocationHandler implements InvocationHandler {
//代理谁:
    //被代理的接口
    private Object target;
//        setter
    public void setTarget(Object target) {
        this.target = target;
    }

    /**
     * @return
     * getClassLoader() 是为了加载类在哪个位置
     * getInterfaces() 表示要代理的接口是哪一个
     * this 代表InvocationHandler本身
     */
//生成得到代理类(固定写法,只需要更改需要实现的接口):
     public Object getProxy(){
       return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
     }
//调用代理程序的一些方法
    //(真正的处理是靠invoke方法执行)处理代理实例,并返回结果
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //动态代理的本质就是使用反射机制实现
        /**
         * 如果这里的msg去设置单独的一个方法,
         * 那么如果我去调用删除方法,就还要在log内更改为delete
         * 但如果通过invoke的method去.出来getName,则只需要去更改前端的调用方法
         * 后端自动的去动态处理输出的内容
         */
        //        log("add");
        log(method.getName());
        Object result = method.invoke(target,args);
        return result;
    }
//    设置新增日志
    public void log(String msg){
        System.out.println("[Debug]使用了"+msg+"方法");
    }

}

用户(租房子的人):

public class Customer {
    public static void main(String[] args) {
//        真实角色
        UserServiceImpl userService = new UserServiceImpl();
//        代理角色(暂时不存在,需要去设置)
        DaiLiInvocationHandler dih = new DaiLiInvocationHandler();
        dih.setTarget(userService);//设置要代理的对象
        UserService proxy = (UserService) dih.getProxy();
        proxy.delete();
    }
}

输出结果:

[Debug]使用了delete方法
删除了一个用户

9.3 代理模式总结

这里写一下在一周目代理模式的总结:

1.        首先动态代理的"反射"问题,得先理解什么叫反射,然后再去看代理模式的代码.

2.        其次代理模式,顾名思义就是需要有一个代理对象,帮助我们的代码去实现业务的分工明确,不会让我们的代码看起来一团糟,所有的业务操作交给代理对象完成;有助于我们在后期的维护工作,不需要改动我们的代码,违反开闭原则(即软件实体应尽量在不修改原有代码的情况下进行扩展).

3.        最后就是要理解其中的架构(套路),很多东西都是不变的,只需要写上去,在后期维护时,更改指定内容即可完成维护,明白真实对象,代理对象,抽象方法和前端之间的调用和实现关系.

 10. 了解反射

1.什么叫反射

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;

对于任意一个对象,都能够调用它的任意一个方法和属性

这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制

2.反射的优点:

反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创和控制任何类的对象,无需提前硬编码目标类

3.反射的缺点

性能问题,使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用

11.AOP

11.1 什么是AOP

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

11.2 Aop在Spring中的作用 

AOP术语:

1、连接点(joinpoint)

        程序执行过程中特定的节点,如方法的调用或异常的执行,通常是一个方法的执行。

2、切入点(pointcut)

        是指需要处理的连接点,所有的方法执行都是连接点,某个特定的连接点就是切入点(被拦截的连接点)

3、目标对象(target)

        是指被通知的对象,即代理的目标对象;

4、通知(advice)

        也被称为增强,是由切面添加到特定的连接点的一段代码,简单来说,通知就是指拦截到的连接点之后所要做的事情,因此通知是切面的具体实现;通知分为前置通知、后置通知、异常通知、最终通知、环绕通知;

5、切面(aspect)

        是指封装横向切到系统功能的类(例如事务处理),是切入点和通知的结合;

6、织入(weave)

        是将切面代码插入到目标对象上,从而生成代理对象的过程;

7,代理(Proxy)

        是指被应用了通知(增强)后,产生一个代理对象;

 11.2.1Spring AOP 的两种代理方法

一种是常规JDK,一种是CGLIB。

当代理对象实现了至少一个接口时,默认使用JDK动态创建代理对象;

当代理对象没有实现任何接口时,就会使用CGLIB方法。

如果实现了接口,强制转换必须用父类接口来定义

因为这部分没有太多可以用言语解释清楚的东西,所以直接看实例

11.3 正题开始,首先导包

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>

其次,创建配置文件"applicationContext.xml"

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

11.4具体分为三种方式

        11.4.1第一种方式:使用原始的SpringAPI接口

首先,编写业务接口和实现类:

public interface UserSerice {
void add();
void delete();
void update();
void select();
}
public class UserServiceImpl implements UserSerice{
    @Override
    public void add() {
        System.out.println("增加了一个用户");
    }

    @Override
    public void delete() {
        System.out.println("删除了一个用户");
    }

    @Override
    public void update() {
        System.out.println("更改了一个用户");
    }

    @Override
    public void select() {
        System.out.println("查询了一个用户");
    }
}

然后就是增强类(前置通知和后置通知)

public class BeforeLog implements MethodBeforeAdvice {
    /**
     *
     * @param method 要执行的目标对象的方法
     * @param args 参数
     * @param target 目标对象
     * @throws Throwable
     */
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(method.getClass().getName()+"的"+method.getName()+"被执行了");
    }
}
public class AfterLog implements AfterReturningAdvice {
    /**
     *
     * @param returnValue 返回值
     * @param method
     * @param args
     * @param target
     * @throws Throwable
     */
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了"+method.getName()+"方法,返回的结果为"+returnValue);
    }
}

再去spring的配置文件"applicationContext.xml"中注册bean, 并实现aop切入的实现.

<!--    注册bean-->
    <bean id="userService" class="com.gcx.Service.UserServiceImpl"/>
    <bean id="beforeLog" class="com.gcx.log.BeforeLog"/>
    <bean id="afterLog" class="com.gcx.log.AfterLog"/>
    <!--    配置AOP:需要导入aop的约束-->
    <aop:config>
<!--        首先需要一个切入点:expression:表达式,execution(要执行的位置 )-->
<!--        要给com.gcx.Service.UserServiceImpl这个类插入方法,但是这个类有很多方法,所以用.*(..) [两个点代表可以有任意个参数]-->
        <aop:pointcut id="pointcut" expression="execution(* com.gcx.Service.UserServiceImpl.*(..))"/>
<!--    执行环绕增加!-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>

最后测试.

public class Mytest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserSerice userService = (UserSerice) context.getBean("userService");
        userService.add();
    }

输出结果

java.lang.reflect.Method的add被执行了
增加了一个用户
执行了add方法,返回的结果为null

由此可以看出它确确实实实在我原有的代码上在想要添加内容的位置新增了内容,即增强代码.

11.4.2 方法二 自定义类来实现Aop

业务实现类不变,依旧是userServiceImpl

第一步:写入我们自己的一个切入类

public class diy {
    public void logBefore(){
        System.out.println("方法执行前");
    }
    public void logAfter(){
        System.out.println("方法执行后");
    }
}

再去spring的配置文件"applicationContext.xml"中注册bean. 

    <bean id="diy" class="com.gcx.log.diy"/>
    <aop:config>
<!--        声明为切面(aspect)类-->
        <aop:aspect ref="diy">
<!--            切入点-->
            <aop:pointcut id="pointcut" expression="execution(* com.gcx.Service.UserServiceImpl.*(..))"/>
<!--            通知-->
            <aop:before method="logBefore" pointcut-ref="pointcut" />
            <aop:after method="logAfter" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

测试

    @Test
    public void LogTest(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserSerice userService = context.getBean("userService", UserSerice.class);
        userService.add();
    }

输出结果

方法执行前
增加了一个用户
方法执行后

没错,哪怕是我们自定义的一个切入类,他也是可以通过切面的方式写入到我们的输出代码中

11.4.3 方式三 使用注解实现

第一步:编写一个注解实现的增强类

/**
 * @Aspect 标注该类是一个切面
 */
@Aspect
public class AnnotationPointCut {
    @Before("execution(* com.gcx.Service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("-----方法执行前-------");
    }
    @After("execution(* com.gcx.Service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("-----方法执行后------");
    }


    /**
     *
     * @param jp 连接点
     */
    //    在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
    @Around("execution(* com.gcx.Service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("======环绕前=====");
//        执行方法,此句代码的执行就表示切入点方法的执行
        Object o = jp.proceed();
        System.out.println("=====环绕后======");
        System.out.println(o);
    }
}

第二步:在Spring配置文件中,注册bean,并增加支持注解的配置

    <bean id="annotationPointCut" class="com.gcx.log.AnnotationPointCut"/>
<!--    开启注解支持  -->
    <aop:aspectj-autoproxy/>
<!--    Spring代理模式有两种:  1. JDK(默认)  2.CGlib
                               当proxy-target-class为false使用默认JDK;
                               当proxy-target-class为true使用CGlib;
                               -->
<!--    <aop:aspectj-autoproxy proxy-target-class="false"/>-->

测试

@Test
    public void LogTest(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserSerice userService = context.getBean("userService", UserSerice.class);
        userService.add();
    }

输出结果

======环绕前=====
-----方法执行前-------
增加了一个用户
=====环绕后======
null
-----方法执行后------

11.5 AOP''心血''总结

切入点声明公式类别:
    1.切点是com.gcx.Service.UserServiceImpl()方法.
        <aop:pointcut id="pointcut1" expression="execution(* com.gcx.Service.UserServiceImpl())"/>
    2.切点是com.gcx.Service类中所有无返回值,参数为空的方法.
        <aop:pointcut id="pointcut2" expression= "execution(void com.gcx.Service.*())"/>
    3.切点是com.gcx.Service类中所有无返回值,对参数无要求的方法.
        <aop:pointcut id="pointcut3" expression="execution(void com.gcx.Service.*(..))"/>
    4.切点是com.gcx.Service类中所有对返回值无要求,参数为空方法.
        <aop:pointcut id="pointcut4" expression="execution(* com.gcx.Service.*())"/>
    5.切点是com.gcx.Service类中所有方法,对返回值,参数均无要求.
        <aop:pointcut id="pointcut5" expression="execution(* com.gcx.Service.*(..))"/>
    6.切点是com.gcx.包中所有类的所有方法,对返回值,参数均无要求.
        <aop:pointcut id="pointcut6" expression="execution(* com.gcx.*.*(..))"/>
    7.切点是com.gcx.包中所有类的UserServiceImpl方法,对返回值,参数均无要求.
        <aop:pointcut id="pointcutbag6" expression="execution(* com.gcx.*.UserServiceImpl(..))"/>
    8.所有类的所有方法,对返回值,参数均无要求。        
        <aop:pointcut id="pointcutbag7" expression="execution(* *(..))"/>

通知策略与执行顺序 :
    1. 前置通知(before):在目标方法执行之前执行执行的通知
    2. 后置通知(after):在目标方法执行之后执行的通知。
    3. 异常通知(after-throwing):在目标方法抛出异常时执行的通知。
    4. 最终通知(after-returning):在目标方法成功执行执行的通知。
    5. 环绕通知(around):目标方法执行之前和之后都可以执行额外代码的通知
    <!--通知的执行顺序与xml文件中配置的顺序有关-->
        //环绕通知的方法,必须遵守如下的定义规则    
        //1:  必须带有一个ProceedingJoinPoint类型的参数,    
        //2:必须有Object类型的返回值    
        //3:在前后增强的业务逻辑之间执行Object v = point.proceed();    
        //4:   方法最后返回 return v;
    例:public Object method5(ProceedingJoinPoint point) throws Throwable {
               System.out.println("~~~~~~~~~~~~~环绕通知before~~~~~~~~~~~");
    //point.proceed (args) 表示执行请求方法,此方法之前表示前置切点,此方法之后表示后置切点,此句代码的执行就表示切入点方法的执行
    Object v = point.proceed();
    System.out.println("~~~~~~~~~~~~~环绕通知after~~~~~~~~~~~~");
    return v;
    } 

 通知规则总结:
    使用xml配置,通知的执行顺序与配置的顺序有关。
    1.使用前置通知,后置通知,最终通知
        1.1 无异常前置通知>切入点方法->后置通知和最终通知(执行顺序根据xml配置的顺序决定)
        1.2 有异常前置通知>切入点方法->后置通知>异常通知
    2.环绕通知,异常通知
        2.1 无异常:环绕通知前置 > 切入点方法 > 环绕通知后置
        2.2 有异常:环绕通知前置 > 切入点方法 > 异常通知
    3.使用所有通知:
        3.1 无异常:执行顺序如下:
            3.1.1 前置通知和环绕通知的前置(依据xml配置顺序)
            3.1.2 切入点方法
            3.1.3 后置通知,最终通知,环绕通知后置(依据xml配置顺序执行有两种情况,一是前置或环绕存在其中之一,二是前置与环绕同时存在且中间没有其他配置)
        3.2 有异常:执行顺序如下:
            3.2.1 前置通知和环绕通知的前置(依据xml配置顺序)
            3.2.2 切入点方法
            3.2.3 后置通知,异常通知(执行顺序同上)
    总结:当前置与环绕之间没有其他配置项时,后置,异常,最终的执行顺序与配置相同

12.声明式事务 

1.事务在项目开发过程非常重要,涉及到数据的"一致性"的问题!
2.事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。
3.事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用.

 12.1 事务的四个属性

1.原子性:事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用
2.一致性:一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中
3.隔离性:可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏
4.持久性:事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中

12.2 Spring中的事务管理 

Spring支持编程式事务管理和声明式的事务管理。
1.声明式事务AOP:
一般情况下比编程式事务好用。
将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。
2.编程式事务:需要在代码中进行事务的管理:
将事务管理代码嵌到业务方法中来控制事务的提交和回滚
缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

12.2.1 事务管理器 

无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
就是 Spring的核心事务管理抽象管理封装了一组独立于技术的方法。

12.3 spring事务传播特性 

事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:
1.propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
2.propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
3.propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
4.propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
5.propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6.propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
7.propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。

12.4 头文件的约束导入 

xmlns:tx="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

12.5 案例 

实体类(因为要写构造器注入,所以要设置空参和有参构造方法)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private int pwd;
}

UserMapper

public interface UserMapper {
//    查询全部用户信息
    List<User> selectUser();
//    添加一个用户
    int addUser(User user);
//    根据id删除一个用户
    int deleteUser(int id);
}

UserMapper.xml(写sql语句)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gcx.mapper.UserMapper">
    <select id="selectUser" resultType="user">
        select *from user;
    </select>
    <insert id="addUser" parameterType="user">
        insert into user (id,name,pwd) values(#{id},#{name},#{pwd})
    </insert>
    <delete id="deleteUser" parameterType="int">
        delete from user where id = #{id}
    </delete>

</mapper>

mybatis-config.xml(mapper配置文件)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--核心配置文件-->
<configuration>
<!--别名管理-->
    <typeAliases>
        <package name="com.gcx.pojo"/>
    </typeAliases>
//使用Spring绑定配置文件,省略配置mapper命名空间
</configuration>

spring-dao.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--DataSource:使用Spring的数据源替换Mybatis的配置  c3p0 dbcp druid
这里使用Spring提供的jdbc:org.springframework.jdbc.datasource-->
    <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>


    <!--    配置sqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource"/>
<!--    绑定Mybatis配置文件-->
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:com/gcx/mapper/*.xml"/>
</bean>
<!--   SqlSessionTemplate:就是我们使用的SqlSession -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--    只能使用构造器注入SqlSessionFactory,因为没有set方法-->
    <constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
    <!--    配置声明式事务-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="datasource"/>
    </bean>
    <!--    结合AOP,实现事务的置入-->
    <!--    配置事务通知:-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--        给那些方法配置事务:-->
        <tx:attributes>
            <!--            配置事务的传播特性:propagation 默认为 REQUIRED-->
            <tx:method name="add" propagation="REQUIRED"/>
            <tx:method name="delete" propagation="REQUIRED"/>
            <tx:method name="update" propagation="REQUIRED"/>
            <!--            read-only:只读-->
            <tx:method name="query" read-only="true"/>
            <!--            配置所有方法-->
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--    配置事务切入-->
    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* com.gcx.mapper.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

  applicationContext.xml(Spring配置文件)

<?xml version="1.0" encoding="UTF-8"?>
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <import resource="spring-dao.xml"/>
    <bean id="userMapper" class="com.gcx.mapper.UserMapperImpl">
        <property name="sqlSession" ref="sqlSession"/>
    </bean>
</beans>

UserMapperImpl(实现类)

/**
 * 另一种写法,直接继承SqlSessionDaoSupport;
 * public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper{}
 * 该方法内部已经写好了构造SqlSessionTemplate类型的变量sqlSession,并创建了SqlSessionFactory工厂
 */
public class UserMapperImpl implements UserMapper{
    private SqlSessionTemplate sqlSession;

    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }
    public List<User> selectUser() {
        User user = new User(7, "小孟", 123134);
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.addUser(user);
        mapper.deleteUser(6);
        return mapper.selectUser();
    }


    public int addUser(User user) {
        return sqlSession.getMapper(UserMapper.class).addUser(user);
    }

    public int deleteUser(int id) {
        return sqlSession.getMapper(UserMapper.class).deleteUser(id);
    }
}

Test 测试

public class Mytest {
    @Test
    public void test(){
        ApplicationContext Context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper userMapper = Context.getBean("userMapper", UserMapper.class);
        List<User> users = userMapper.selectUser();

        for (User user : users) {
            System.out.println(user);

        }
    }
}

 输出结果

User(id=1, name=小顾, pwd=123456)
User(id=2, name=小董, pwd=123666)
User(id=3, name=小杨, pwd=1234567)
User(id=4, name=小孟, pwd=1234567)
User(id=5, name=小鑫, pwd=122223)
User(id=7, name=小孟, pwd=123134)

13.阶段性自我总结

经过将近长达两周的自学,明白了什么是Spring,其中的两大面试要点''IOC控制反转''和"AOP切面编程",是学起来比较吃力的;明白了Spring的配置文件该如何写,里边都包含了什么内容,是用来做什么的,bean的注入的N多方式,如何使用Spring自动连接数据库,不需要像JDBC一样繁琐,两种不同的代理模式,AOP的各种实现代码增强的方法,各种通知以及声明式事务.

最庆幸的是在写这些代码的过程中遇到了很多问题,很多报错,但并没有放弃,各种百度,csdn查找问题的解决方法,所以继续加油,坚持下去,一定会有所进步!

猜你喜欢

转载自blog.csdn.net/m0_74135466/article/details/128227021