Spring

一、Spring

1.1 简介

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

用于整合现有的技术,使各技术使用更加便捷。

官网:https://spring.io/projects/spring-framework

官方下载地址:https://repo.spring.io/libs-release-local/org/springframework/spring/

GitHub:https://github.com/spring-projects/spring-framework

  • 两个重要的依赖包
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.12.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.12.RELEASE</version>
</dependency>

1.2 优点

  • Spring是一个轻量级的、非入侵式的框架;
  • Spring是一个开源的免费的框架(容器);
  • 控制反转(Ioc)、面向切面编程(AOP);
  • 支持事务的处理,对框架整合的支持。

1.3 组成

Spring七大模块
在这里插入图片描述

  • 核心容器:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能 , 集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理任何支持 AOP的对象。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  • Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

1.4 拓展

  1. SpringBoot
  • 一个快速开发的脚手架
  • 基于SpringBoot可以快速的开发单个微服务
  • 约定大于配置
  1. Spring Cloud
  • Spring Cloud是基于SpringBoot实现的

学习SpringBoot的前提是,完全掌握Spring和SpringMVC

二、IOC容器

控制反转IoC(Inversion of Control)可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做**依赖注入(Dependency Injection,简称DI**),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中,将对象的创建转移给第三方。

2.1 IOC理论推导

下面注意看代码中接口调用的区别

  • 首先看使用IOC之前的方法:

    • UserDao 接口
    public interface UserDao {
        void getUser();
    }
    
    • UserDaoimpl 实现类
    public class UserDaoimpl implements UserDao{
        public void getUser(){
            System.out.println("默认获取用户的数据");
        }
    }
    
    • UserService 业务接口
    public interface UserService {
        void getUser();
    }
    
    • UserServicelmpl 业务实现类
    public class UserServiceimpl implements UserService {
    
        private UserDao userDao = new UserDaoimpl();
    
        //实际上调用的是接口层的方法
        public void getUser(){
            userDao.getUser();
        }
    }
    
    • 测试类
    public class MyTest {
        public static void main(String[] args) {
    
            //用户实际调用的是业务层,dao层不需要接触
            UserServiceimpl userServiceimpl = new UserServiceimpl();
            userServiceimpl.getUser();
        }
    }
    

现在dao层只存在一个实现类,后续如果需要存在多个实现类,当用户调用不同接口时,要随时修改代码。

  • 现在使用set注入:

    • 首先创建另一个实现类mysqlDaoimpl
    public class UserDaoimpl implements UserDao{
        public void getUser(){
            System.out.println("默认获取用户的数据");
        }
    }
    
    • 然后进行测试调用
    public class MyTest {
        public static void main(String[] args) {
    
            //用户实际调用的是业务层,dao层不需要接触
            UserServiceimpl serviceimpl = new UserServiceimpl();
            serviceimpl.setUserDao(new UserDaoimpl());
            /**如果想要用另一个实现类MysqlDaoimpl去实现,直接在这里更改对象就可以
            *serviceimpl.setUserDao(new MysqlDaoimpl())
            */
            serviceimpl.getUser();
        }
    }
    

这里程序提供接口,用户去调用接口就可以了,从控制上发生了根本性的转变。

2.2 HelloSpring

2.2.1 创建POJOs实体类Hello

public class Hello {
    private String str;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    @Override
    public String toString() {
        return "Hello{" +
                "str='" + str + '\'' +
                '}';
    }
}

2.2.2 配置元数据

  • 基于 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"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="..." class="...">
            <!-- collaborators and configuration for this bean go here -->
        </bean>
    
        <!-- more bean definitions go here -->
    
    </beans>
    
    • id属性是标识单个 bean 定义的字符串。
    • class属性定义 bean 的类型并使用完全限定的类名。
  • 创建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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--使用spring创建对象,在spring中这些都成为Bean-->
        <bean id="hello" class="com.rainlx.pojo.Hello">
            <property name="str" value="spring"/>  <!--为Hello中的属性填充值-->
        </bean>
    
    </beans>
    

2.2.3 实例化容器

提供给ApplicationContext构造函数的位置路径是资源字符串,这些资源字符串使容器可以从各种外部资源(例如本地文件系统,Java CLASSPATH等)加载配置元数据。

public class MyTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
    }
}

这里是将IoC容器实例化,而在Spring容器中配置元数据,其实是创建、实例化、设置属性值的一个过程

<bean id="hello" class="com.rainlx.pojo.Hello">
    <property name="str" value="spring"/> 
</bean>
 <!--
	在spring之前创建实例化一个对象的方法是:类型 变量名 = new 类型()
	在spring中,id = 变量名,class = new的对象,property设置属性的值
 -->

进行测试

public class MyTest {
    public static void main(String[] args) {
        //Spring容器
        ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        Hello hello = (Hello) context.getBean("hello");
        System.out.println(hello.toString());
    }
}
  • 总结:

Spring创建对象、设置属性值,需要实现不同的操作,只需要在xml配置文件中进行。

实际上就是利用Spring的IoC容器Management对象

2.3 IOC创建对象的方式

  1. 使用无参构造创建对象

    <bean id="user" class="com.rainlx.pojo.User">
        <property name="name" value="rainlx"/>
    </bean>
    
  2. 使用有参构造创建对象

    <!--第一种:下标赋值-->
    <bean id="Bean01" class="com.rainlx.pojo.User">
        <constructor-arg index="0" value="rainlx"/>
    </bean>
    
    <!--第二种:通过类型创建,当实体类中有两个相同类型的参数时不可用这种方法-->
    <bean id="Bean02" class="com.rainlx.pojo.User">
        <constructor-arg type="java.lang.String" value="rainlx"/>
    </bean>
    
    <!--第三种:通过参数名创建,主要使用这种方法-->
    <bean id="Bean03" class="com.rainlx.pojo.User">
        <constructor-arg name="name" value="rainlx"/>
    </bean>
    

**总结:**配置文件加载的时候,容器(ApplicationContext context)中管理的对象就已经被初始化了

2.4 Spring配置说明

Beans配置

<!--
id:bean的唯一标识符,相当于对象名;
class:bean对象对应的全限定名;
name:别名,可以取多个别名,中间用逗号、分号、空格隔开
-->
<bean id="Bean01" class="com.rainlx.pojo.User" name="likealias,bean01">
    <constructor-arg index="0" value="rainlx"/>
</bean>

<!--
name:Bean中创建的对象
alias:别名
-->
<alias name="Bean01" alias="bean"/>

<!--import用于整合不同的配置文件,在不同配置文件中创建的对象,可以直接在总的配置文件中使用-->
<import resource="beans.xml"/>

三、依赖注入DI

3.1 构造器注入

第2.2、2.3节的方法

3.2 Set注入

依赖:Bean对象的创建依赖于容器

注入:Bean对象中所有的属性,由容器注入

  1. Student实体类

    package com.rainlx.pojo;
    
    import java.util.*;
    
    public class Student {
        private String name;        //value赋值
        private Address address;    //ref赋值
        private String[] books;
        private List<String> hobby;
        private Map<String,String> card;
        private Set<String> games;
        private Properties info;
        private String wife;
    
        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> getHobby() {
            return hobby;
        }
    
        public void setHobby(List<String> hobby) {
            this.hobby = hobby;
        }
    
        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 Properties getInfo() {
            return info;
        }
    
        public void setInfo(Properties info) {
            this.info = info;
        }
    
        public String getWife() {
            return wife;
        }
    
        public void setWife(String wife) {
            this.wife = wife;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", address=" + address +
                    ", books=" + Arrays.toString(books) +
                    ", hobby=" + hobby +
                    ", card=" + card +
                    ", games=" + games +
                    ", info=" + info +
                    ", wife='" + wife + '\'' +
                    '}';
        }
    }
    
  2. Address实体类

    package com.rainlx.pojo;
    
    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 + '\'' +
                    '}';
        }
    }
    
  3. 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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="student" class="com.rainlx.pojo.Student">
            <!--第一种:普通值注入,value-->
            <property name="name" value="rain"/>
    
            <!--第二种:Bean注入,ref-->
            <property name="address" ref="address"/>
    
            <!--第三种:数组-->
            <property name="books">
                <array>
                    <value>Java入门</value>
                    <value>Java进阶</value>
                    <value>Java入神</value>
                </array>
            </property>
    
            <!--第四种:集合-->
            <property name="hobby">
                <list>
                    <value>打球</value>
                    <value>画画</value>
                    <value>看书</value>
                </list>
            </property>
    
            <!--第五种:Map-->
            <property name="card">
                <map>
                    <entry key="银行卡" value="888888888888888"/>
                    <entry key="电话卡" value="18888888888"/>
                </map>
            </property>
    
            <!--第六种:set-->
            <property name="games">
                <set>
                    <value>泡泡堂</value>
                    <value>王者荣耀</value>
                </set>
            </property>
    
            <!--第七种:Properties-->
            <property name="info">
                <props>
                    <prop key="学号">888888</prop>
                    <prop key="班级">1001</prop>
                </props>
            </property>
    
            <!--第八种:Properties-->
            <property name="wife">
                <null/>
            </property>
        </bean>
    
        <bean id="address" class="com.rainlx.pojo.Address">
            <property name="address" value="中国"/>
        </bean>
    </beans>
    
  4. 测试类

    import com.rainlx.pojo.Student;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MyTest {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            Student student = (Student) context.getBean("student");
            System.out.println(student.toString());
        }
    }
    

    输出结果:

    Student{
    	name='rain', 
    	address=Address{address='中国'}, 
    	books=[Java入门, Java进阶, Java入神],
        hobby=[打球, 画画, 看书], 
        card={
        	银行卡=888888888888888, 
        	电话卡=18888888888
        }, 
        games=[泡泡堂, 王者荣耀], 
        info={
        	学号=888888, 
        	班级=1001
        },
        wife='null'
    }
    

3.3 拓展方式注入

3.3.1 p命名空间

xmlns:p="http://www.springframework.org/schema/p"

p-namespace 允许您使用bean元素的属性(而不是嵌套的<property/>元素)来描述协作 Bean 的属性值,或同时使用这两者

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

    <!--第一个使用标准 XML 格式,第二个使用 p 命名空间-->
    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="[emailprotected]"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="[emailprotected]"/>
</beans>

注:使用p命名空间需要有无参构造

3.3.2 c命名空间

c-namespace 允许使用内联属性来配置构造函数参数,而不是嵌套的constructor-arg元素。

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

    <bean id="thingOne" class="x.y.ThingTwo"/>
    <bean id="thingTwo" class="x.y.ThingThree"/>

    <!--第一个使用标准 XML 格式,第二个使用 c 命名空间-->
    <bean id="thingOne" class="x.y.ThingOne">
        <constructor-arg ref="thingTwo"/>
        <constructor-arg ref="thingThree"/>
        <constructor-arg value="[emailprotected]"/>
    </bean>

    <bean id="thingOne" class="x.y.ThingOne" c:thingTwo-ref="thingTwo" c:thingThree-ref="thingThree" c:email="[emailprotected]"/>

</beans>

注:使用p命名空间要求实体类中有有参构造

3.4 bean的作用域

ScopeDescription
singleton(默认)将每个 Spring IoC 容器的单个 bean 定义范围限定为单个对象实例。
prototype将单个 bean 定义的作用域限定为任意数量的对象实例。
request将单个 bean 定义的范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有一个在单个 bean 定义后面创建的 bean 实例。仅在可感知网络的 Spring ApplicationContext中有效。
session将单个 bean 定义的范围限定为 HTTP Session的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
application将单个 bean 定义的范围限定为ServletContext的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
websocket将单个 bean 定义的范围限定为WebSocket的生命周期。仅在可感知网络的 Spring ApplicationContext上下文中有效。
  • singleton单例模式(Spring默认)

    <bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
    
  • prototype原型模式

    每次从容器中get的时候,都会生成一个新的对象

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

四、Bean自动装配

自动装配是Spring满足bean依赖的一种方式

Spring会在上下文中自动寻找,并自动给bean装配属性

4.1 环境搭建

  • Dog实体类

    package com.rainlx.pojo;
    
    public class Dog {
        public void bark(){
            System.out.println("汪~");
        }
    }
    
  • Cat实体类

    package com.rainlx.pojo;
    
    public class Cat {
        public void bark(){
            System.out.println("喵~");
        }
    }
    
  • People实体类

    package com.rainlx.pojo;
    
    public class People {
        private Cat cat;
        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;
        }
    }
    

4.2 byName、byType

<bean id="cat" class="com.rainlx.pojo.Cat"/>
<bean id="dog" class="com.rainlx.pojo.Dog"/>

<!--
byName:自动在容器上下文查找和自己对象set方法后面的值对应的beanid
byType:自动在容器上下文查找和自己对象属性类型相同的bean,但必须保证这个类型全局唯一
-->
<bean id="people" class="com.rainlx.pojo.People" autowire="byName">
    <property name="name" value="rainnice"/>
</bean>

<bean id="people" class="com.rainlx.pojo.People" autowire="byType">
    <property name="name" value="rainnice"/>
</bean>

解释:

`id="people"`这个<bean>设置了byName自动装配,并且People实体类中包含了setName(..)方法,那么spring将自动查找一个名为people的bean定义并使用它来设置属性

在本例中,People实体类包含了setDog( )、setCat( )方法,在`id="people"`的bean中设置自动装配后,就会自动查找`id="dog"`和`id="cat"`的bean,完成自动装配。

4.3 注解自动装配

使用注解条件:

  1. 导入约束

    包含context名称空间

  2. 配置注解的支持

    <?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
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:annotation-config/>
    
    </beans>
    

4.3.1 @Autowired

@Autowired可以直接在属性上使用,也可以在set方法上使用

使用@Autowired可以没有set方法,但要有get,前提是自动装配的属性在IOC容器中存在,且名字一致

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean id="cat" class="com.rainlx.pojo.Cat"/>
    <bean id="dog" class="com.rainlx.pojo.Dog"/>
    <bean id="people" class="com.rainlx.pojo.People"/>
</beans>
  • 实体类
package com.rainlx.pojo;

import org.springframework.beans.factory.annotation.Autowired;

public class People {

    @Autowired
    private Cat cat;
    @Autowired
    private Dog dog;

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

    public Cat getCat() {
        return cat;
    }

    public Dog getDog() {
        return dog;
    }

}
  • 测试
import com.rainlx.pojo.People;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest {

    @Test
    public void test(){
        ApplicationContext Context = new ClassPathXmlApplicationContext("applicationContext.xml");
        People people = Context.getBean("people", People.class);
        people.getCat().bark();
        people.getDog().bark();
    }
}
  • 如果显式定义了Autowired的required属性为false,说明这个对象可以为null,否则不允许为空

  • 如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解完成的时候,可以使用@Qualifier(value=“xxx”)去配合@Autowired的使用,指定一个唯一的bean对象注入。

    <bean id="cat01" class="com.rainlx.pojo.Cat"/>
    <bean id="cat02" class="com.rainlx.pojo.Cat"/>
    
    //在xml中配置了id=cat和id=cat01的bean,通过使用@Qualifier来指定bean对象注入
    @Autowired
    @Qualifier(value = "cat01")
    private Cat cat;
    

4.3.2**@Resource**

<bean id="cat01" class="com.rainlx.pojo.Cat"/>
<bean id="cat02" class="com.rainlx.pojo.Cat"/>
<bean id="dog" class="com.rainlx.pojo.Dog"/>
public class People(){
    @Resource(name = "cat01")	<!--name指定唯一对象-->
    private Cat cat;
    @Resource
    private Dog dog;
    
}

总结:

  • @Resource和@Autowired都是用来自动装配的,都可以放在属性字段上

  • @Autowired默认通过byType实现,然后通过byName实现

  • @Resource默认通过byName实现,如果找不到名字,通过byType实现,两个都找不到则报错

注解汇总

  • @Autowired:自动装配,先类型,后名字

    如果@Autowired不能唯一装配属性,则需要通过@Qualifier(value=“xxx”)指定唯一的bean

  • @Nullable:字段标记了这个注解,说明这个字段可以为null

  • @Resource:自动装配,先名字,后类型

  • @Component:组件,放在类上,说明这个类被Spring管理装配成bean了

  • @value:放在属性/属性的set()方法上,用来给属性字段赋值

五、Spring注解开发

使用注解开发,首先要进行一些配置

  1. 保证aop的包导入
    在这里插入图片描述

  2. 导入context约束和支持

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--指定要扫描的包,这个包下的注解才会生效-->
    <context:component-scan base-package="com.rainlx.pojo"/>
    <context:annotation-config/>
</beans>

5.1 属性注入

//等价于<bean id="user" class="com.rainlx.pojo.User"/>
//Component组件
@Component
public class User {

    //相当于<property name="user" value="rainlx"/>
    @Value("rainlx")
    public String name;
}

5.2 衍生的注解

@Component

在web开发中,按照mvc三层架构分成:

  • dao【@Repository】

  • service【@Service】

  • controller【@Controller】

    这四个注解的功能是一样的,都是将某个类注册到Spring中,装配bean

5.3 作用域

@Scope注解用来限定作用域,主要有以下几个基本作用域:

  • singleton单例模式:全局有且仅有一个实例
  • prototype原型模式:每次获取Bean的时候会有一个新的实例
  • Web(reqeust、session、globalsession)
    • request:针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效
    • session:针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效
    • globalsession:类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义

总结:

  • xml与注解:
    • xml更加万能,适用于任何场合,维护简便
    • 注解,只能使用自己的类,维护复杂
  • xml与注解最佳实践:
    • xml用来管理bean
    • 注解负责完成属性注入

六、Java配置Spring

6.1 基本概念

使用Java配置代替xml配置Spring,主要使用@Configuration注解的类和@Bean注解的方法

  • 使用@Configuration的类,表示这个类是一个配置类,等同于ApplicationContext.xml文件

  • @Bean注解用于指示方法实例化,配置和初始化要由 Spring IoC 容器 Management 的新对象

用一个简单的例子说明如何使用java配置Spring

  • 实体类

    //@Component注解:将下面的类注册到容器中
    @Component
    public class User {
        private String name;
    
        public String getName() {
            return name;
        }
    
        @Value("rainlx")
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
  • 新建一个Config.java的配置文件

    //下面的类注册到容器中被Management
    @Configuration
    public class ProConfig {
    
        //@Bean注解与<bean/>元素具有相同的作用
        //方法名就是xml配置文件bean标签中的id属性
        //方法的返回值就是bean标签中的class属性
        @Bean
        public User getUser(){
            return new User();
        }
    }
    
  • 测试类

    public class MyTest {
        public static void main(String[] args) {
            //如果完全使用java配置类,就只能通过 AnnotationConfig 上下文获取容器,通过配置类的class对象加载。
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProConfig.class);
            User user = context.getBean("getUser",User.class);
            System.out.println(user.getName());
        }
    }
    

如果有多个config.java配置文件,可以使用@Import注解将所有配置文件导入到同一个文件中

@Import(****.class)

七、代理模式

代理模式的分类:

  • 静态代理

  • 动态代理

7.1 静态代理1

角色分析:

  • 抽象角色:一般用接口或抽象类来解决

  • 真实角色:被代理的角色

  • 代理角色:代理真实角色

  • 客户:访问代理对象的人

用一个实例说明静态代理各个角色及之间的关系

  1. 接口

    package com.rainlx.demo01;
    
    //提供一个“租房”的业务
    public interface Rent {
    }
    
  2. 真实角色

    package com.rainlx.demo01;
    
    //房东要参与“租房”业务,所以要继承这个业务
    public class Host implements Rent{
    
        public void rent(){
            System.out.println("房东出租房屋");
        }
    }
    
  3. 代理角色

    package com.rainlx.demo01;
    
    //中介代理了房东的业务,所以也要继承
    public class Proxy implements Rent{
    
        private Host host;      //代理角色(中介)代理了真实角色(房东)
    
        public Proxy() {
        }
    
        public Proxy(Host host) {
            this.host = host;
        }
    
        public void rent(){
            host.rent();        //代理角色拥有了真实角色的方法,这里代理代替房东去出租房屋
            seeHouse();
            sign();
        }
    
        //代理角色还可以提供公共业务
        public void seeHouse(){
            System.out.println("看房子");
        }
        public void sign(){
            System.out.println("签合同");
        }
    }
    
  4. 客户

    package com.rainlx.demo01;
    
    public class Client {
        public static void main(String[] args) {
            Host host = new Host();
            Proxy proxy = new Proxy(host);      //方法中的对象是代理角色代理的真实对象
            proxy.rent();
        }
    }
    

代理模式的好处:

  • 使真实角色不用去关注一些公共的业务
  • 所有的公共业务交给了代理角色
  • 公共业务发生扩展的时候,方便集中管理

缺点:

  • 一个真实角色就会有一个代理角色,代码量会翻倍,降低开发效率

7.2 静态代理2

例子:一个具有增删改查功能的业务,现在要在没个功能前加一个日志功能

思路:在不改动原来代码的条件下,增加一个代理原业务的角色,然后在代理角色中增加新的日志功能

  1. 接口

    package com.rainlx.demo02;
    
    public interface UserService {
        public void add();
        public void delete();
        public void update();
        public void query();
    }
    
  2. 原业务类

    package com.rainlx.demo02;
    
    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("查询用户");
        }
    }
    
  3. 代理业务

    package com.rainlx.demo02;
    
    //代理也要有原有的增删改查功能业务
    public class UserServiceProxy implements UserService{
    
        //代理原业务UserServiceImpl
        private UserServiceImpl userService;
    
        //将原业务对象userService注入到代理对象中,要用set方法,
        public void setUserService(UserServiceImpl userService) {
            this.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+"方法");
        }
    }
    
  4. 调用业务类

    package com.rainlx.demo02;
    
    public class Client {
        public static void main(String[] args) {
            UserServiceImpl userService = new UserServiceImpl();
            UserServiceProxy proxy = new UserServiceProxy();
            proxy.setUserService(userService);	//将原业务对象userService注入到代理对象中
    
            proxy.add();
            proxy.delete();
        }
    }
    
    运行结果:
    【DEBUG】使用了add方法
    增加用户
    【DEBUG】使用了delete方法
    删除用户
    

7.3 动态代理1

  • 动态代理的代理类是动态生成的,不是直接写好的
  • 动态代理分两大类
    • 基于接口——JDK动态代理
    • 基于类——cglib

还是通过租房子的例子说明动态代理中的各个角色

  1. 接口

    package com.rainlx.demo03;
    
    public interface Rent {
        public void rent();
    }
    
  2. 真实角色

    package com.rainlx.demo03;
    
    public class Host implements Rent {
    
        public void rent(){
            System.out.println("房东出租房屋");
        }
    }
    
  3. 程序处理角色

    package com.rainlx.demo03;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    //通过这个类自动生成代理类
    public class ProxyInvocationHandler implements InvocationHandler {
    
        //被代理的接口
        private Rent rent;
    
        public void setRent(Rent rent) {
            this.rent = rent;
        }
    
        /**
         * 生成得到代理类,这块的代码是固定的,只需将第二个参数修改成要代理的接口就可以
         * 下面三个参数依次是:loader、interfaces、h
         * loader:用哪个类加载器去加载代理对象
         * interfaces:动态代理类要实现的接口
         * h:动态代理方法在执行时,会调用h里面的invoke方法执行
         */
        public Object getProxy(){
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
        }
    
    
        //处理代理实例,并返回结果
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
            //动态代理的本质就是使用反射机制实现
            Object result = method.invoke(rent, args);
            return result;
        }
    }
    
  4. 客户

    package com.rainlx.demo03;
    
    
    
    public class Client {
        public static void main(String[] args) {
            Host host = new Host();
            ProxyInvocationHandler handler = new ProxyInvocationHandler();
            handler.setRent(host);      //通过”程序处理角色“的set方法将要代理的对象注入到handler中
    
            Rent proxy = (Rent) handler.getProxy();
            proxy.rent();
        }
    }
    

7.4 动态代理2

例子:设置一个万能的动态代理,代理demo02中的真实对象

  1. 程序处理角色

    package com.rainlx.demo04;
    
    import com.rainlx.demo03.Rent;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    //通过这个类自动生成代理类
    public class ProxyInvocationHandler implements InvocationHandler {
    
        //被代理的接口
        private Object target;
    
        public void setTarget(Object target) {
            this.target = target;
        }
    
        //生成代理类
        public Object getProxy(){
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        }
    
    
        //处理代理实例,并返回结果
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            log(method.getName());      //getName自动获取执行方法的名称
            Object result = method.invoke(target, args);
            return result;
        }
    
        public void log(String msg){
            System.out.println("执行了"+msg+"方法");
        }
    }
    
  2. 客户

    package com.rainlx.demo04;
    
    import com.rainlx.demo02.UserService;
    import com.rainlx.demo02.UserServiceImpl;
    
    public class Client {
        public static void main(String[] args) {
            //真实角色
            UserServiceImpl userService = new UserServiceImpl();
            ProxyInvocationHandler handler = new ProxyInvocationHandler();
            handler.setTarget(userService);
    
            UserService proxy = (UserService) handler.getProxy();
            proxy.add();
            proxy.delete();
        }
    }
    运行结果:
    执行了add方法
    增加用户
    执行了delete方法
    删除用户
    

代理模式的好处:

  • 使真实角色不用去关注一些公共的业务
  • 所有的公共业务交给了代理角色
  • 公共业务发生扩展的时候,方便集中管理
  • 一个动态代理类代理的是一个接口,即对应的一类业务
  • 一个动态代理类可以代理多个类,只要是实现同一个接口即可

八、AOP

8.1 什么是AOP

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

AOP中的概念

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。是一个类
  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
  • Pointcut(切入点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。要织入的位置
  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。是类中的一个方法
  • Target(目标对象):织入 Advice 的目标对象。要织入的对象
  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

8.2 Spring实现AOP

使用AOP,首先需要导入一个依赖包

<dependencies>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

8.2.1 方式一:使用Spring API接口

现在希望实现一个在CRUD方法的前后插入日志的功能,设计方式如下:

  • 先写好接口接口实现类、日志类(要织入的对象
  • 然后通过Spring API接口的方式实现AOP,在applicationContext.xml配置文件中进行AOP织入
  • 在配置文件中说明,切入点:需要在哪个地方执行代码;增强:执行的是哪一段代码。
    • PS:标记的几项内容是AOP必备的
  1. 接口

    package com.rainlx.service;
    
    public interface UserService {
        public void add();
        public void delete();
        public void update();
        public void select();
    }
    
  2. 接口实现类

    package com.rainlx.service;
    
    public class UseServiceImpl 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 select() {
            System.out.println("查询用户");
        }
    }
    
  3. before日志类

    package com.rainlx.log;
    
    
    import org.springframework.aop.MethodBeforeAdvice;
    
    import java.lang.reflect.Method;
    
    public class Log implements MethodBeforeAdvice {
    
        /**
         * @param method:要执行的目标对象的方法
         * @param args:参数
         * @param target:目标对象
         */
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println(target.getClass().getName()+"的"+method.getName()+"方法被执行了");
        }
    }
    
  4. after日志类

    package com.rainlx.log;
    
    import org.springframework.aop.AfterReturningAdvice;
    
    import java.lang.reflect.Method;
    
    public class AfterLog implements AfterReturningAdvice {
    
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
            System.out.println("执行了"+method.getName()+"方法,结果为"+returnValue);
        }
    }
    
  5. 配置文件(在这里配置pointcut)

    <?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">
    
        <!--注册bean-->
        <bean id="userService" class="com.rainlx.service.UseServiceImpl"/>
        <bean id="log" class="com.rainlx.log.Log"/>
        <bean id="afterlog" class="com.rainlx.log.AfterLog"/>
    
        <!--方式一:使用Spting AIP接口-->
        <!--配置AOP,需要导入AOP的约束-->
        <aop:config>
            <!--切入点: expression:表达式,execution(要织入的位置)-->
            <aop:pointcut id="pointcut" expression="execution(* com.rainlx.service.UserServiceImpl.*(..))"/>
    
            <!--增强:advice-ref:使用的方法,pointcut-ref:切入点-->
            <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
            <aop:advisor advice-ref="afterlog" pointcut-ref="pointcut"/>
        </aop:config>
    </beans>
    
  6. 测试类

    import com.rainlx.service.UserService;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            //动态代理,代理的是接口
            UserService userService = context.getBean("userService", UserService.class);
            userService.add();
        }
    }
    运行结果:
    com.rainlx.service.UseServiceImpl的add方法被执行了
    增加用户
    执行了add方法,结果为null
    
    • AOP中的pointcut expression表达式:
    任意公共方法的执行:
    execution(public * *(..))
    
    任何一个以“set”开始的方法的执行:
    execution(* set*(..))
    
    AccountService 接口的任意方法的执行:
    execution(* com.xyz.service.AccountService.*(..))
    
    定义在service包里的任意方法的执行:
    execution(* com.xyz.service.*.*(..))
    
    定义在service包和所有子包里的任意类的任意方法的执行:
    execution(* com.xyz.service..*.*(..))
    
    定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:
    execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))
    
    PS:最靠近(..)的为方法名,靠近.*(..))的为类名或者接口名,如上例的JoinPointObjP2.*(..))
    

    execution(* com.xyz.service..*.*(..))表达式包括5部分:

    • execution():表达式主体
    • 第一个*号:表示返回类型, *号表示所有的类型
    • 包名:表示需要拦截的包名,后面的两个点表示当前包和当前包的所有子包,com.xyz.service包、子孙包下所有类的方法
    • 第二个*号:表示类名, *号表示所有的类
    • *(…):最后这个星号表示方法名, *号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

8.2.2 方式二:使用自定义类

主要过程是定义切面,切面要包括切入点、增强

  1. 自定义日志类

    package com.rainlx.diy;
    
    public class DiyPointCut {
        public void before(){
            System.out.println("执行前的日志:*******");
        }
        public void after(){
            System.out.println("执行后的日志:*******");
        }
    }
    
  2. 配置文件(配置切入点、切面)

    <?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">
    
        <!--方式二:自定义类-->
        <bean id="diypointcut" class="com.rainlx.diy.DiyPointCut"/>
        
        <aop:config>
            <!--自定义切面,ref:要引用的类-->
            <aop:aspect ref="diypointcut">
                <!--切入点-->
                <aop:pointcut id="pointcut" expression="execution(* com.rainlx.service.UserServiceImpl.*(..))"/>
                <!--通知-->
                <aop:before method="before" pointcut-ref="pointcut"/>
                <aop:after method="after" pointcut-ref="pointcut"/>
    
            </aop:aspect>
        </aop:config>
    </beans>
    
  3. 测试类

    import com.rainlx.service.UserService;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserService userService = context.getBean("userService",UserService.class);
            userService.add();
        }
    }
    运行结果:
    执行前的日志:*******
    增加用户
    执行后的日志:*******
    

8.2.3 方式三:使用注解

接口类、接口实现类还是上面例子中的。

  1. 自定义一个类

    package com.rainlx.diy;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect	//标注这个类是一个切面
    public class AnnotationPointCut {
    
        @Before("execution(* com.rainlx.service.UserServiceImpl.*(..))")
        public void before(){
            System.out.println("====方法执行前====");
        }
    
        @After("execution(* com.rainlx.service.UserServiceImpl.*(..))")
        public void after(){
            System.out.println("====方法执行后====");
        }
    
        @Around("execution(* com.rainlx.service.UserServiceImpl.*(..))")
        //在环绕通知中,可以给定一个参数,用于启动目标方法执行的
        public void around(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("环绕开始");				  //前置通知
            Object proceed = joinPoint.proceed();		 //目标方法
            System.out.println("环绕结束");				  //后置通知
        }
    }
    
  2. 在配置文件中注册bean,并添加AOP注解支持

    <?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">
    
        <!--方式三:注解-->
        <bean id="annotationPointCut" class="com.rainlx.diy.AnnotationPointCut"/>
        <!--开启注解支持。proxy-target-class(默认为false,使用JDK实现,若为true,使用的则是cglib)-->
        <aop:aspectj-autoproxy proxy-target-class="true"/>
    </beans>
    
  3. 测试类

    import com.rainlx.service.UserService;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserService userService = context.getBean("userService",UserService.class);
            userService.add();
        }
    }
    运行结果:
    环绕开始
    ====方法执行前====
    增加用户
    ====方法执行后====
    环绕结束
    

九、整合MyBatis

erviceImpl.*(…))")
public void after(){
System.out.println(“方法执行后”);
}

   @Around("execution(* com.rainlx.service.UserServiceImpl.*(..))")
   //在环绕通知中,可以给定一个参数,用于启动目标方法执行的
   public void around(ProceedingJoinPoint joinPoint) throws Throwable {
       System.out.println("环绕开始");				  //前置通知
       Object proceed = joinPoint.proceed();		 //目标方法
       System.out.println("环绕结束");				  //后置通知
   }

}


2. 在配置文件中注册bean,并添加AOP注解支持

```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">

    <!--方式三:注解-->
    <bean id="annotationPointCut" class="com.rainlx.diy.AnnotationPointCut"/>
    <!--开启注解支持。proxy-target-class(默认为false,使用JDK实现,若为true,使用的则是cglib)-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
  1. 测试类

    import com.rainlx.service.UserService;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserService userService = context.getBean("userService",UserService.class);
            userService.add();
        }
    }
    运行结果:
    环绕开始
    ====方法执行前====
    增加用户
    ====方法执行后====
    环绕结束
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值