Spring 学习笔记

Spring笔记

1、Spring概况

Spring雏形:interface21
下载地址:https://repo.spring.io/release/org/springframework/spring/

1.1、Spring的优点

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

1.2、Spring的本质

  • 控制反转,依靠依赖注入的方式来实现。
    以一个servcie对象为例,即是service暴露注入接口(构造,set方法),由spring配置对象注入(设置)给该service对象,这样可以做到Service层专注业务,不需要改变自身代码,只需要在调用(注入)的时候改变对象,即可改变service的具体实现,service面向接口编程,由service主动构建对象到被动接收外部注入的对象。同时Spring作为容器,会自己构建对象,这些对象可以作为参数来注入。对象由Spring来创建,管理,装配。

1.3、Spring Maven 依赖



    org.springframework
    spring-webmvc
    5.2.2.RELEASE




    org.springframework
    spring-jdbc
    5.2.2.RELEASE

1.4、组成(七大模块)

Spring 学习笔记_第1张图片
1.4、七大模块.png
  • Spring Core (核心容器):

    核心容器提供spring框架的基础功能。核心容器的主要组件是BeanFactory,它是工厂模式的实

    现。BeanFactory使用控制反转(IoC)模式将应用程序的配置和依赖性规范的应用程序代码分开。

  • Spring Context(上下文):

    spring上下文是一个配置文件,向spring提供上下文信息,spring上下文包括企业服务,例如

    JBDI、EJB、电子邮件、国际化、校验和调度功能。

  • Spring AOP:

通过配置管理特性、AOP模块直接面向切面的棉城功能集成到了Spring框架中。所以,可以很容

易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中

的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务

管理集成到应用程序中。

  • 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。

2、控制反转(IoC)

2.1、IoC的本质

​ IoC (Inversion of Control 控制反转)是一种设计思想,通过描述 (XML或注解) 并通过第三方去生成或获取特定对象的方式,在Spring中实现控制反转的是IoC容器,DI (Dependency Injection 依赖注入)是实现IoC的一种方式。

Spring 学习笔记_第2张图片
2.1_1.png

IoC是Spring框架的核心内容,使用多种方式完美的实现了IoC,可以使用XML配置,也可以使用注解,新版本的Spring也可以零配置实现IoC。
​ Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从Ioc容器中取出需要的对象。

Spring 学习笔记_第3张图片
2.1_2.png

​ 采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。

2.2、如何理解IoC控制反转

  • 谁控制谁?
    传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由IoC容器来控制对象的创建,所以是IoC容器控制对象。
  • 控制什么?
    主要控制外部资源的获取(不只是对象也包括文件等等)
  • 为何是反转?
    有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转,而反转则是 由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受
    依赖对象,所以是反转。
  • 哪些方面反转了?
    所依赖对象的获取被反转了。

2.3、IoC能做什么

​ IoC不是一种技术,只是一种思想,一个重要的面对对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合、难测试,有了IOC容器后,把创建和查找依赖对象的控制权交给容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,也方便测试,利于功能复用,使得程序体系架构变得灵活。

​ 其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

​ IoC很好的体现了面向对象设计法则之一 ———— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

2.4、实例与创建对象的方式

2.4.1、 IoC原型实例

1、先写一个UserDao接口

public interface UserDao {
    public void getUser();
}

2、再写Dao的实现类

public class UserDaoImpl implements UserDao {
    @Override
    public void getUser() {
        System.out.println("获取用户数据");
    }
}

3、然后写UserService的接口

public interface UserService {
    public void getUser();
}

4、最后写Service的实现类

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();
    @Override
    public void getUser() {
        userDao.getUser();
    }
}

5、测试

@Test
public void test(){
    UserService service = new UserServiceImpl();
    service.getUser();
}

以上是传统Java SE程序的写法,修改如下:

增加一个Userdao的实现类 UserDaoMySqlImpl.java

public class UserDaoMySqlImpl implements UserDao {
    @Override
    public void getUser() {
        System.out.println("MySql获取用户数据");
    }
}

要使用MySql时 , 就需要去service实现类里面修改对应的实现

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoMySqlImpl();
    @Override
    public void getUser() {
        userDao.getUser();
    }
}

假设, 再增加一个Userdao 的实现类 UserDaoOracleImpl.java

public class UserDaoOracleImpl implements UserDao {
    @Override
    public void getUser() {
        System.out.println("Oracle获取用户数据");
    }
}

​ 这时要调用Oracle, 就需要去service实现类里面修改对应的实现,如果类似的需求非常多,这种方式便不适用,耦合性非常高。

如何解决 ?

利用set方法 , 修改代码如下:

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

修改测试类

@Test
public void test(){
    UserServiceImpl service = new UserServiceImpl();
    service.setUserDao(new UserDaoMySqlImpl());
    service.getUser();
    //如果又想用Oracle去实现呢?
    service.setUserDao(new UserDaoOracleImpl());
    service.getUser();
}

​ 区别:在使用set方法之前,所有需要的对象都是由程序员主动创建,使用set方法注入之后,主动权由程序员转移到了调用者,程序员不用再理会对象的创建,可以更专注业务的实现,耦合性大大降低,这便是IoC 的原型。

2.4.2、第一个Spring程序

1、导入Maven依赖项

  
        
            org.springframework
            spring-webmvc
            5.2.2.RELEASE
        

2、编写User实体类 User.java

public class User {
    private String name;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "User{" +"name='" + name + '\'' +'}';
    }
}

3、编写spring配置文件 applicationContext.xml



    
        
    

4、创建测试类

public class UserTest {
    public static void main(String[] args){
        //解析applicationContext.xml文件,生成并管理相应的Bean对象
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //getBean的参数为spring配置文件中bean的id
        User user = (User) applicationContext.getBean("user");
        System.out.println(user.toString());
    }
}

输出


Spring 学习笔记_第4张图片
2.4.2_OUTPUT.png

扩展
在上述IoC原型实例中新增加Spring配置文件 applicationContext.xml



    
    
    
        
        
        
    

测试

@Test
public void test2(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserServiceImpl serviceImpl = (UserServiceImpl) context.getBean("ServiceImpl");
    serviceImpl.getUser();
}

如此一来就实现了解耦,要实现不同的操作,只需要在xml配置文件中修改配置。

2.4.3、IoC创建对象的方式

2.4.3.1、通过无参构造方法

1、修改 2.4.2第一个spring程序 实例中 User 类,增加User显式无参构造方法,其他不变

public class User {
    private String name;
    public User() {
        System.out.println("user无参构造方法");
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }

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

2、测试类

public class UserTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) applicationContext.getBean("user");
        System.out.println(user.toString());
    }
}

输出

Spring 学习笔记_第5张图片
2.4.3_OUTPUT.png

结论:在执行 getBean 时,user对象已经创建好。(通过无参构造)

2.4.3.2、通过有参构造方法

1、修改 第一个spring程序 实例中 User 类,增加User有参构造方法

public class User {
    private String name;
    public User(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }

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

2、修改applicationContext.xml配置文件,针对有参构造方法有三种配置方式

   
    
         
        
    
 
    
        
        
    
   
    
        
    

3、测试类

public class UserTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) applicationContext.getBean("user");
        System.out.println(user.toString());
    }
}

输出


Spring 学习笔记_第6张图片
2.5_OUTPUT.png

结论:管理的对象在配置文件加载的时候初始化完成。

2.5、依赖注入(DI)

概念
DI (Dependency Injection) 依赖注入:组件之间的依赖关系由容器在运行期决定,也就是说,由容器动态将某个依赖关系注入到组件中。依赖注入并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过注入机制,只需要通过简单的配置,无需任何代码就可以指定目标需要的资源,完成自身的业务逻辑,不需要关心具体的资源来自何处,由谁实现。

​IoC在系统运行中,会动态的向某个对象提供它(对象)所需的其他对象,这一点是通过DI(依赖注入)来实现的,比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

如何理解DI?

  • 谁依赖谁?

    应用程序依赖IoC容器

  • 为什么需要依赖?

    应用程序需要IoC容器来提供对象需要的外部资源

  • 谁注入谁?

    IoC容器注入应用程序的某个对象

  • 注入了什么?

    注入某个对象所需要的外部资源(包括对象、资源、常量数据)

IoC Service Provider 为被注入对象提供被依赖对象有如下几种方式

  • 构造器注入
  • set方法注入
  • 自动装配注入
  • 注解注入

2.5.1、构造器注入

(参考 2.4.3.1 跟 2.4.3.2 实例)

2.5.2、set方法注入

要求被注入的属性必须有set方法,set方法的方法名由 set + 属性名首字母大写(如果属性的类型为boolean,则为 is + 属性名首字母大写)。

实例:
Address.java

public class Address {
    private String address;
    public String getAddress() {
        return address;
    }

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

Student.java

public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List hobby;
    private Map card;
    private Set games;
    private String wife;
    private Properties info;

    /**省略 getter 跟 setter 方法**/
    
    public void show() {
        StringBuilder sb = new StringBuilder()
                .append("name = ").append(name).append("\n")
                .append("address = ").append(address.getAddress()).append("\n")
                .append("hobby = ").append(hobby).append("\n")
                .append("card = ").append(card).append("\n")
                .append("games = ").append(games).append("\n")
                .append("wife = ").append(wife).append("\n")
                .append("info = ").append(info).append("\n")
                .append("books = ");
        if (books != null)
            for (String book : books)
                sb.append(String.format("《%s》 ", book));
        System.out.println(sb.toString());
    }
}

1、常量注入

    
        
    

2、Bean注入 ( ref 引用 )

    
        
    
    
        
        
    

3、数组注入

 
      
            Java从入门到夺门而出
            Spring概述
      
 

4、List注入


     
          敲代码
          看影视
     
 

5、Map注入

  
       
           
           
        
  

6、Set注入


      
          LOL
          csgo
      

7、Null注入


8、Properties注入


     
          20191219001
          
     
 

测试类

   @Test
    public void diTest(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Student student = (Student) context.getBean("student");
        student.show();
    }

输出


Spring 学习笔记_第7张图片
2.5.3_OUTPUT_1.png

2.5.3、自动装配

​ 自动装配(autowire)是使用Spring满足bean依赖的一种方式,可以在applicationContext.xml配置文件的bean标签添加 autowire 属性,使得Spring可以自动在上下文给对象注入属性。

自动装配做的两件事:

  • 组件扫描 (component scanning):Spring自动发现应用上下文中所创建的bean。
  • 按照规则装配 (autowiring):Spring自动满足bean之间的依赖。(IoC/DI)

Spring中bean有三种装配机制:

  • 在xml配置文件中显式配置
  • 在java中显式配置
  • 隐式的bean发现机制和自动装配

自动装配方式:

  • byName
  • byType
  • constructor

实例环境

public class Cat {
    public void shout() {
        System.out.println("miao~");
    }
}
public class Dog {
    public void shout() {
        System.out.println("wang~");
    }
}
public class User {
    private String name;
    private Cat cat;
    private Dog dog;
    /**省略 getter 跟 setter 方法**/
}
    
    
    
        
        
        
    
public class AutowireTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) context.getBean("user");
        System.out.println(String.format("%s的宠物", user.getName()));
        user.getCat().shout();
        user.getDog().shout();
    }
}

输出


Spring 学习笔记_第8张图片
2.5.3_OUTPUT_2.png
2.5.3.1、byName

1、修改applicationContext.xml配置文件中的bean如下:

    
    
    
        
    

2、测试输出


Spring 学习笔记_第9张图片
2.5.3.1_OUTPUT_1.png

3、继以上修改再将cat 的bean id 改为 catXXX

    
    
    
        
    

4、测试输出 (空指针异常)


Spring 学习笔记_第10张图片
2.5.3.1_OUTPUT_2.png

使用byName小结:
byName属性会自动在上下文里找和自己属性对应set方法后边的名字(首字母改为小写)一样的bean id。若bean id 跟set方法后面的名字不一样,则报空指针异常。(id必须唯一)

2.5.3.2、byType

1、修改xml配置文件如下

    
    
    
        
    

2、测试输出(正常)

3、继续修改xml配置文件如下:

   
    
    
    
        
    

4、测试输出(NoUniqueBeanDefinitionException 异常)

5、继续修改xml配置文件如下:

    
    
    
        
    

6、测试输出 (正常)

使用byType小结:
byType会自动在上下文里找和自己属性类型相同的bean,类型要唯一,要被注入的bean没有id值也可以,class要唯一。

2.5.3.3、constructor

1、修改xml配置文件如下:

    
    
    
        
    

2、修改User.java,添加构造方法,如下:

public class User {
    private String name;
    private Cat cat;
    private Dog dog;

    public User(Cat cat, Dog dog) {
        this.cat = cat;
        this.dog = dog;
    }
    /**省略 getter 跟 setter 方法**/
}

3、测试输出


Spring 学习笔记_第11张图片
2.5.3.3_OUTPUT.png

使用constructor小结:
constructor会尝试把它的构造函数的参数与配置文件中 beans 名称中的一个进行匹配和连线。如果找到匹配项,它会注入这些 bean,否则,会抛出异常。

2.5.4、注解注入

以2.5.3实例继续

2.5.4.1、@Autowired

@Autowired 注解表示按照类型自动装配(属于Spring规范),不支持id匹配。可以写在字段上,也可以写在setter方法上(使用@Autowired 注解可以省略set方法)。默认情况下要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false,如:@Autowired(required=false) 。

1、在spring配置文件中引入context文件头

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

http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd

2、开启属性注解支持


3、修改后完整的xml配置文件如下:




     

    
    
    
        
    

4、修改User.java,将set方法去掉,添加 @Autowired 注解

public class User {
    private String name;
    @Autowired
    private Cat cat;
    @Autowired(required = false) //如果允许对象为null,可以设置required为false,默认为true
    private Dog dog;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Cat getCat() {
        return cat;
    }
    public Dog getDog() {
        return dog;
    }
}

5、测试输出


Spring 学习笔记_第12张图片
2.5.4.1_OUTPUT.png
2.5.4.2、@Qualifier

@Qualifier 不能单独使用,需跟 @Autowired 配合使用,两者配合使用后可根据 byName 的方式来自动装配。

1、修改xml配置文件中的bean id不为类的默认名字,如下:

 
 

2、在User.java类中添加 @Qualifier 注解

    @Autowired
    @Qualifier(value = "cat22")
    private Cat cat;
    @Autowired(required = false) //如果允许对象为null,可以设置required为false,默认为true
    @Qualifier(value = "dog11")

3、测试输出成功

2.5.4.3、@Resource

@Resource 注解(属于J2EE规范)为 @Autowired 和 @Qualifier 的结合版。先尝试以 byName 方式对属性进行查找装配,若不成功,则以 byType 的方式进行装配,若两者都不成功,则报异常。

@Resource 注解 默认按照名称匹配,名称可以通过name属性指定,如果没指定,当注解写在字段上时,默认取字段名进行查找,注解写在setter方法上默认取属性名进行装配。 当找不到与名称匹配的bean时才按照类型进行装配。需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

@Autowired先byType,@Resource先byName。

1、修改xml配置文件,如下:

    
    
    

2、修改User.java类

    @Resource(name = "cat22")
    private Cat cat;
    @Resource
    private Dog dog;

3、测试输出成功
4、再修改xml配置文件如下:

    
    

5、修改User.java类,只保留 @Resource 注解

    @Resource
    private Cat cat;
    @Resource
    private Dog dog;

6、测试输出成功

2.6、Bean部分配置说明

别名
alias 设置别名 , 为bean设置别名 , 可以设置多个别名






    

2.7、Bean的作用域

​ 在Spring中,组成应用程序的主题及有Spring IoC容器所管理的对象,被称为Bean。换言之,Bean就是由IoC容器初始化、装配以及管理的对象。

Bean的作用域如下表:


Spring 学习笔记_第13张图片
2.7.png

以上4种作用域中的request跟session只能用在基于web的Spring ApplicationContext环境中。

2.7.1、Singleton

  • 当一个bean的作用域为Singleton,那么Spring IoC容器中值存在一个共性的bean对象,并且所有对bean的请求只要id跟bean定义的相匹配,就只会返回bean的同一实例。
  • Singleton是单例类型,在创建容器时就同时自动创建了bean的对象,每次获取到的对象都是同一个。
  • Singleton作用域是Spring中的缺省作用域。
  • 较适合单线程使用

User.java

public class User {
    private String name;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "User{" +"name='" + name + '\'' +'}';
    }
}

在xml配置文件中将bean定义成Singleton

  
       ... ...
  

测试

public class UserTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) applicationContext.getBean("user");
        User user2 = (User) applicationContext.getBean("user");
        System.out.println(user == user2);
    }
}

输出


Spring 学习笔记_第14张图片
2.7.1_OUTPUT.png

2.7.2、Prototype

  • 当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的 getBean() 方法) 时都会创建一个新的bean实例。
  • Prototype是原型类型,在创建容器时并没有实例化,而是在获取bean的时候才会创建一个对象,每次获取到对象都不是同一个对象。
  • 对有状态的bean应该使用prototype作用域,而对无状态的bean则应使用Singleton作用域。

在xml配置文件中将bean定义成Prototype

  
       ... ...
  
public class UserTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) applicationContext.getBean("user");
        User user2 = (User) applicationContext.getBean("user");
        System.out.println(user == user2);
    }
}

测试输出


Spring 学习笔记_第15张图片
2.7.2_OUTPUT.png

2.7.3、Request

  • 当一个bean的作用于为Request,表示在一次HTTP请求中,一个bean定义对应一个实例。即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。
  • 该作用域仅在基于web的Spring ApplicationContext环境下有效。

在xml配置文件中将bean定义成Request

  
       ... ...
  

2.7.4、Session

  • 当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。
  • 该作用域仅在基于web的Spring ApplicationContext环境下有效。

在xml配置文件中将bean定义成Session

  
       ... ...
  

​ 针对某个HTTP Session,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与Request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域的bean也会被废弃掉。

3、Spring注解式开发

Spring 在 4.0 版本后引入了全注解开发模式,去除了编写繁琐的配置文件,使用配置类即可进行bean的扫描和装配。

3.1、简单使用和说明

步骤

1、使用注解模式必须先确保 aop包 的引入:


Spring 学习笔记_第16张图片
3.1_aop包引入.png

2、在配置文件引入context约束:



    

3、配置扫描哪些包下的注解:


4、使用和说明



       
       

/**
@Component 注解将此类标注为Spring的一个组件,并导入到IoC容器中。
此处相当于在xml配置文件中 
为了更好的分层,Spring可以使用其他三个注解(功能一样),分别是:
    @Controller     对Web层实现类标注
    @Service        对Service层实现类标注
    @Repository     对Dao层实现类标注
*/
@Component("cat") 
public class Cat {
    public void shout(){
        System.out.println("miao~");
    }
}
@Component("dog")
public class Dog {
    public void shout(){
        System.out.println("wang~");
    }
}
@Component("user") //@Component("值") 注解将此类标注为Spring的一个组件,值相当于XML配置文件中的name属性。
@Scope("Singleton") //可用 @Scope 注解标注 Singleton 或 Prototype 模式
public class User {
   // @Value注解相当于配置文件中 ,使用@Value注解可以不需要set方法。
    @Value("woitumi") 
    private String name;
    private int count;
    @Autowired // @Autowired按照类型自动装配,也可使用 @Resource("cat")指定id 
    private Cat cat;
    @Autowired
    private Dog dog;

    public String getName() {
        return name;
    }

    public Cat getCat() {
        return cat;
    }

    public Dog getDog() {
        return dog;
    }
    
    @Value("2")  // 如果有set方法也可以将 @Value("值") 设在set方法上。
    public void setCount(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }
}
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) context.getBean("user");
        System.out.println(String.format("%s有%d只宠物", user.getName(), user.getCount()));
        user.getCat().shout();
        user.getDog().shout();
    }
}

输出


3.1_OUTPUT.png

XML配置文件与注解装配比较
1、XML可以适用于任何场景,结构清晰,维护方便。
2、注解开发简单方便,但在非自己写的类里难以使用。

3.2、基于 JavaConfig 配置

​ JavaConfig 原来是 Spring 的一个子项目,它通过 Java 类的方式提供 Bean 的定义信息,在 Spring4 版本后, JavaConfig 已正式成为 Spring 的核心功能 。

User.java类

public class User {
    private String name;
    private Car car;

    public String getName() {
        return name;
    }

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

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

Car.java类 :

public class Car {
    private String brand;
    private String type;
    //使用构造方法注入值
    public Car(String brand, String type) {
        this.brand = brand;
        this.type = type;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

新建一个config配置包,编写一个MyConfig配置类,如下:

@Configuration //代表这是配置类
public class MyConfig {  
    @Bean("myCar")  //@Bean("myCar") 注解会向容器中注册一个叫 myCar 的对象
    public Car getCar() {
        return new Car("保时捷", "911");
    }
    
    //向容器中注册装配一个 user 对象,并通过byType的方式注入car,对象car需要有set方法
    @Bean(value = "user", autowire = Autowire.BY_TYPE)
    public User getUser() {
        User user = new User();
        user.setName("woitumi");
        return user;
    }
}

写法二:

public class User {
    @Value("woitumi") //使用  @Value("值") 注解将值注入,相应的字段可以不需要set方法
    private String name;
    private Car car;

    public String getName() {
        return name;
    }

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
}
public class Car {
    @Value("保时捷") //使用  @Value("值") 注解将值注入,可以不需要set方法
    private String brand;
    @Value("911")
    private String type;
    
    public String getBrand() {
        return brand;
    }
    
    public String getType() {
        return type;
    } 
}
@Configuration //@Configuration 代表这是配置类
// @ComponentScan("com.woitumi.springtest") //指定扫描包 (了解可以这么用)
// @Import(MyConfig2.class) //融合多个配置类
public class MyConfig {
    @Bean("myCar")  //@Bean("myCar") 注解会向容器中注册一个叫 myCar 的对象
    public Car getCar() {
        return new Car();
    }
 //向容器中注册装配一个 user 对象,并通过byType的方式注入car,对象car需要有set方法
    @Bean(value = "user", autowire = Autowire.BY_TYPE)
    public User getUser() {
        return new User();
    }
}

测试类

    @org.junit.Test
    public void test() {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        User user = context.getBean("user", User.class);
        System.out.println(String.format("%s的车是%s%s",
                user.getName(),
                user.getCar().getBrand(),
                user.getCar().getType()
        ));
    }

输出


3.2_OUTPUT.png

4、代理模式

​ 为什么要学习代理模式?因为AOP的底层机制是动态代理。

​ 代理模式作为23种经典设计模式之一,其比较官方的定义为“为其他对象提供一种代理以控制对这个对象的访问”,简单点说就是,之前A类自己做一件事,在使用代理之后,A类不直接去做,而是由A类的代理类B来去做。代理类其实是在之前类的基础上做了一层封装。

它的设计思路是:定义一个抽象角色,让代理角色和真实角色分别去实现它。

代理模式的核心作用

  • 符合开闭原则,不用修改被代理者的任何代码就能扩展新的功能。
  • 方便项目的扩展和维护。

角色分类

  • 抽象角色:一般使用接口或抽象类来实现。
  • 真实角色:被代理角色,实现抽象角色,定义真实角色所需要的业务逻辑,供代理角色调用。
  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑来实现抽象方法,并可在前后添加新操作。
  • 客户:使用代理角色来实现一些操作。

代理模式分为

  • 静态代理
  • 动态代理

4.1、静态代理

什么是静态代理?

  • 代理者和被代理者都实现了相同接口。(或继承了相同的父类)
  • 代理者包含了一个被代理者的对象。
  • 调用功能时,代理者会调用被代理者的功能,同时附加新的操作。

静态代理的缺点

  • 只适合一种业务,如果有新的业务,就必须创建新的接口和新的代理,工作量变大了,开发效率降低。

实例一

Rent.java 抽象角色

//抽象角色:租房
public interface Rent {
    public void rent();
}

Host.java 真实角色

//真实角色: 房东,房东要出租房子
public class Host implements Rent{
    public void rent() {
        System.out.println("房屋出租");
    }
}

Proxy.java 代理角色

//代理角色:中介
public class Proxy implements Rent {
    private Host host;
    public Proxy() {
    }
    public Proxy(Host host) {
        this.host = host;
    }

    //租房
    public void rent(){
        seeHouse();
        host.rent();
        fare();
    }
    //看房
    public void seeHouse(){
        System.out.println("带房客看房");
    }
    //收中介费
    public void fare(){
        System.out.println("收中介费");
    }
}

Client.java 客户

//租客通过中介租到房东的房子
public class Client {
    public static void main(String[] args) {
        //房东要租房
        Host host = new Host();
        //中介帮助房东
        Proxy proxy = new Proxy(host);
        //租客找中介
        proxy.rent();
    }

分析: 在这个过程中,客户直接接触的是中介,就如同现实生活中的样子,你看不到房东,但是客户通过中介租到了房东的房子,这就是静态代理模式。

实例二

1、创建抽象角色,比如增删改查的业务逻辑

//抽象角色:增删改查业务
public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}

2、需要一个真实对象来完成增删改查操作

//真实对象,完成增删改查操作的人
public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("增加");
    }

    public void delete() {
        System.out.println("删除");
    }

    public void update() {
        System.out.println("更新");
    }

    public void query() {
        System.out.println("查询");
    }
}

3、如果需要增加一个日志功能,如何实现?
思路一:在实现类上增加代码。(麻烦)
思路二:使用代理,在不改变原有业务逻辑的情况下实现日志功能

4、定义一个代理角色来实现日志功能

//代理角色,在这里面增加日志的实现
public class UserServiceProxy implements UserService {
    private UserServiceImpl userService;

    public void setUserService(UserServiceImpl userService) {
        this.userService = userService;
    }

    public void add() {
        log("add");
        userService.add();
    }

    public void delete() {
        log("delete");
        userService.delete();
    }

    public void update() {
        log("update");
        userService.update();
    }

    public void query() {
        log("query");
        userService.query();
    }

    public void log(String msg){
        System.out.println("执行了" + msg + "方法");
    }

}

5、测试类

public class Client {
    public static void main(String[] args) {
        //真实业务
        UserServiceImpl userService = new UserServiceImpl();
        //代理类
        UserServiceProxy proxy = new UserServiceProxy();
        //使用代理类实现日志功能
        proxy.setUserService(userService);
        proxy.add();
    }
}

4.2、动态代理

动态代理的特点

  • 在不修改原有类的基础上,为原有类添加新的功能。
  • 不需要依赖某个具体业务。

动态代理的好处

  • 可以使得真实角色更加纯粹,不需再关注一些公共的业务。
  • 公共的业务由代理完成,实现了业务的分工。
  • 公共业务发生扩展时变得更加集中和方便。

动态代理分为

  • JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,需要指定一个类加载器,然后生成的代理对象实现类的接口或类的类型,接着处理额外功能。
  • CGLib动态代理:动态利用sam的开源包,对代理对象的Class文件加载进来,通过修改其字节码生成的子类来出来,CGLib是基于继承父类生成的代理类。

JDK代理和CGLib代理的区别

  • JDK动态代理的被代理者必须实现任何接口。
  • CGLib动态代理不用实现接口,主要对指定的类生成一个子类,覆盖其中的方法,添加额外的功能,通过继承实现。所以该类方法不能用final来修饰。

动态代理的实现步骤

  • 代理类需要实现 InvocationHandler 接口。
  • 实现 invoke 方法。
  • 通过 Proxy 类的 newProxyInstance 方法来创建代理对象

JDK动态代理实例一 :

以 4.1租房实例 继续

//抽象角色:租房
public interface Rent {
    public void rent();
}

Host . java 真实角色

//真实角色: 房东,房东要出租房子
public class Host implements Rent{
    public void rent() {
        System.out.println("房屋出租");
    }
}

ProxyInvocationHandler. java 即代理角色

public class ProxyInvocationHandler implements InvocationHandler {
    private Rent rent;

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

    //生成代理类,第二个参数获取要代理的抽象角色
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                rent.getClass().getInterfaces(),this);
    }

    // proxy : 代理类 method : 代理类的调用处理程序的方法对象.
    // 处理代理实例上的方法调用并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        //核心:利用反射实现
        Object result = method.invoke(rent, args);
        fare();
        return result;
    }

    public void seeHouse(){
        System.out.println("带房客看房");
    }

    public void fare(){
        System.out.println("收中介费");
    }

}

Client . java

//租客
public class Client {
    public static void main(String[] args) {
        //被代理对象(真实角色)
        Host host = new Host();
        //代理实例的调用处理程序
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        pih.setRent(host); 
        Rent proxy = (Rent)pih.getProxy(); //创建代理对象
        proxy.rent();
    }
}

JDK动态代理实例二:

//用户管理接口
public interface UserManager {
    void addUser(String name, String password);
    void delUser(String name);
}
 //用户管理实现类,实现用户管理接口
public class UserManagerImpl implements UserManager {
    public void addUser(String name, String password) {
        System.out.println("addUser:" + name + ":" + password);
    }

    public void delUser(String name) {
        System.out.println("delUser:" + name);
    }
}
//JDK动态代理实现InvocationHandler接口
public class JdkProxy implements InvocationHandler {
    private Object target;
    //定义获取代理对象方法
    public Object getJdkProxy(Object object) {
        target = object;
        //JDK动态代理只能针对实现了接口的类进行代理
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                                      target.getClass().getInterfaces(), this);
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理,重写invoke方法调用被代理者的方法");
        Object result = method.invoke(target,args);
        System.out.println("JDK动态代理,可以执行附加操作");
        return result;
    }
}

    @Test
    public void JdkProxy() {
        JdkProxy jdkProxy = new JdkProxy(); //实例化JdkProxy对象
        //获取代理对象
        UserManager userManager = (UserManager) jdkProxy.getJdkProxy(new UserManagerImpl());
        userManager.addUser("woitumi", "wanantumi");
 }
Spring 学习笔记_第17张图片
4.2_OUTPUT_1.png

CGLib动态代理实例

首先需要导入asm跟cglib的 Maven依赖

        
        
            org.ow2.asm
            asm
            7.2
        

        
        
            cglib
            cglib
            3.3.0
        
public class CglibProxy implements MethodInterceptor {
    private Object target;
    //重写拦截方法
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) 
        throws Throwable {
        System.out.println("CGLib动态代理,重写invoke方法调用被代理者的方法");
        Object result = method.invoke(target, objects);
        System.out.println("CGLib动态代理,可以执行附加操作");
        return result;
    }

    //定义获取代理对象方法
    public Object getCglibProxy(Object object) {
        target = object;
        Enhancer enhancer = new Enhancer();
        //设置父类,Cglig是针对指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(object.getClass());
        enhancer.setCallback(this); //设置回调
        return enhancer.create(); //创建并返回代理对象
    }
}
    @Test
    public void CglibProxy() {
        CglibProxy cglibProxy = new CglibProxy();
        UserManager userManager = (UserManager) cglibProxy.getCglibProxy(new UserManagerImpl());
        userManager.delUser("张三");
    }
Spring 学习笔记_第18张图片
4.2_OUTPUT_2.png

扩展
实现JDK动态代理的工具类

public class ProxyInvocationHandler implements InvocationHandler { 
    private Object object;
    /**
     * 创建代理对象
     * @param object 被代理者
     * @return 代理者
     */
    public Object createProxy(Object object) {
        this.object = object;
        //Proxy.newProxyInstance创建动态代理的对象,传入被代理对象的类加载器,接口,InvocationHandler对象
        //注意 Proxy 是 java.lang.reflect.Proxy 包下的 Proxy
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), 
                                      object.getClass().getInterfaces(), this);
    }

    //调用被代理者方法,同时可以添加新操作
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //TODO 调用被代理者的方法
        Object result = method.invoke(object, args);
        //TODO 可添加新的操作
        return result;
    }
}

使用(如上述租房实例)

    @Test
    public void rentHost() {
        Host host = new Host();
        Rent proxy = (Rent) new ProxyInvocationHandler().createProxy(host);
        proxy.rent();
    }

5、面向切面编程(AOP)

5.1、AOP概述

​ AOP(Aspect Oriented Programming)意为:面向切面编程,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置上。面向切面编程是一种编程范式,它作为OOP面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。AOP采取横向抽取机制,取代了传统纵向继承体系的重复性代码。

AOP把软件的功能模块分为两个部分:

  • 核心关注点
  • 横切关注点

​ 业务处理的主要功能为核心关注点,需要拓展的功能为横切关注点,利用AOP可以对业务逻辑的各个关注点进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率。

Spring 学习笔记_第19张图片
5.png

​ 例如,在一个业务系统中,用户登录是基础功能,凡是涉及到用户的业务流程都要求用户进行系统登录。如果把用户登录功能代码写入到每个业务流程中,会造成代码冗余,维护也非常麻烦,当需要修改用户登录功能时,就需要修改每个业务流程的用户登录代码,这种处理方式显然是不可取的。比较好的做法是把用户登录功能抽取出来,形成独立的模块,当业务流程需要用户登录时,系统自动把登录功能切入到业务流程中,以下为用户登录功能切入到业务流程示意图:

Spring 学习笔记_第20张图片
5_1.png

AOP相关概念

  • 切面(Aspect):横切关注点,被模块化的特殊对象,是一个类。
  • 连接点(Joinpoint):程序执行过程中的某一行为。
  • 通知(Advice):切面 对于某个 连接点 所产生的动作。类中的方法。
  • 切入点(Pointcut):匹配连接点,在AOP中通知和一个切入点表达式关联。
  • 目标对象(Target Object):被一个或多个切面所通知的对象。
  • AOP代理(AOP Proxy):向目标对象应用通知之后创建的对象。
Spring 学习笔记_第21张图片
5.1_2.png

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:

Spring 学习笔记_第22张图片
5.1_3.png
  • 前置通知(Before advice):在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛异常。
  • 正常返回通知(After returning advice):在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
  • 异常返回通知(After throwing advice):在连接点抛出异常后执行。
  • 返回通知(After finally advice):在连接点执行完成后执行,不管是正常还是抛异常,都会返回通知中的内容。
  • 环绕通知(Around advice):环绕通知围绕在连接点前后。这是一个强大的通知类型,能在方法调用前后自定义一些操作,环绕通知还需要负责决定是继续处理连接点(调用ProceedingJoinPoint的proceed方法)还是中断执行。

5.2、OOP与AOP对比理解

OOP (Object Oriented Programming) 面向对象编程,AOP (Aspect Oriented Programming) 面向切面编程。
纵向关系OOP,横向角度AOP

举个小例子:

设计一个日志打印模块。按 OOP 思想,我们会设计一个打印日志 LogUtils 类,然后在需要打印的地方引用即可。

public class ClassA {
    private void initView() {
        Log.d(TAG, "onInitView");
    }
}

public class ClassB {
    private void onDataComplete(Bean bean) {
        Log.d(TAG, bean.attribute);
    }
}

public class ClassC {
    private void onError() {
        Log.e(TAG, "onError");
    }
}

看起来没有任何问题是吧?

但是这个类是横跨并嵌入众多模块里的,在各个模块里分散得很厉害,到处都能见到。从对象组织角度来讲,我们一般采用的分类方法都是使用类似生物学分类的方法,以「继承」关系为主线,我们称之为纵向,也就是 OOP。设计时只使用 OOP思想可能会带来两个问题:

  1. 对象设计的时候一般都是纵向思维,如果这个时候考虑这些不同类对象的共性,不仅会增加设计的难度和复杂性,还会造成类的接口过多而难以维护(共性越多,意味着接口契约越多)。
  2. 需要对现有的对象 动态增加 某种行为或责任时非常困难。

而AOP就可以很好地解决以上的问题,怎么做到的?除了这种纵向分类之外,我们从横向的角度去观察这些对象,无需再去到处调用 Log 打印日志了,声明哪些地方需要打印日志,这个地方就是一个切面,AOP 会在适当的时机把打印语句插进切面。

// 只需要声明哪些方法需要打印 log,打印什么内容
public class ClassA {
    @Log(msg = "onInitView")
    private void initView() {
    }
}

public class ClassB {
    @Log(msg = "bean.attribute")
    private void onDataComplete(Bean bean) {
    }
}

public class ClassC {
    @Log(msg = "onError")
    private void onError() {
    }
}

​ 如果说 OOP 是把问题划分到单个模块的话,那么 AOP 就是把涉及到众多模块的某一类问题进行统一管理。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。利用 AOP 思想,这样对业务逻辑的各个部分进行了隔离,从而降低业务逻辑各部分之间的耦合,提高程序的可重用性,提高开发效率。


Spring 学习笔记_第23张图片
5.2.png

OOP 与 AOP 的区别

  • 面向目标不同:OOP面向名词领域,AOP面向动词领域。
  • 思想结构不同:OOP是纵向结构,AOP是横向结构。
  • 注重方面不同:OOP注重业务逻辑单元的划分,AOP偏重业务处理过程中的某个步骤或阶段。

OOP 与 AOP 两者是一个相互补充和完善的关系。

5.3、使用

使用AOP织入,需要导入 aspectjweaver 依赖包

  
        
            org.aspectj
            aspectjweaver
            1.9.5
        

在applicationContext.xml文件中配置AOP需要引入AOP约束,如下:




5.3.1、通过Spring API实现
//业务接口
public interface DataManager {
    public void add();
    public void delete();
    public void update();
    public void query();
}
//业务实现类
public class DataManagerImpl implements DataManager {
    public void add() {
        System.out.println("添加");
    }

    public void delete() {
        System.out.println("删除");
    }

    public void update() {
        System.out.println("更新");
    }

    public void query() {
        System.out.println("查询");
    }
}
//前置增强类
public class BeforeLog implements MethodBeforeAdvice {
    /**
     * @param method 要执行的目标对象的方法
     * @param objects 被调用的方法的参数
     * @param target 目标对象
     */
    public void before(Method method, Object[] objects, Object target) throws Throwable {
        System.out.println(String.format("执行 %s 类的 %s 方法前的日志",
                target.getClass().getName(),
                method.getName()
        ));
    }
}
//后置增强类
public class AfterLog implements AfterReturningAdvice {
    /**
     * @param returnValue 返回值
     * @param method 被调用的方法
     * @param objects 被调用方法的对象参数
     * @param target 被调用的目标对象
     */
    public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) 
        throws Throwable {
        System.out.println(String.format("执行 %s 类的 %s 方法后的日志,返回值为 %s",
                target.getClass().getName(),
                method.getName(),
                returnValue
        ));
    }
}


    
    
    
    
    
    
        
        
        
        
        
    


   @Test
    public void aopTest() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        DataManager manager = (DataManager) context.getBean("dataManager");
        manager.add();
    }
Spring 学习笔记_第24张图片
5.3.2_PUTPUT.png

附录:execution 表达式含义:

execution(* com.woitumi.aoptest.DataManagerImpl.*(..))

5.3.2、自定义类实现AOP

继上述例子 业务接口跟业务实现类不变,修改增加

//自定义切入类
public class CustomPointcut {
    public void before() {
        System.out.println("before");
    }

    public void after() {
        System.out.println("after");
    }
}

 
    
    
        
        
        
    

Spring 学习笔记_第25张图片
5.3.2_OUTPUT_2.png
5.3.3、使用注解实现AOP

继以上实例修改增加

@Aspect
public class AnnotationPointcut {
    @Before("execution(* com.woitumi.aoptest.DataManagerImpl.*(..))")
    public void before() {
        System.out.println("before");
    }

    @After("execution(* com.woitumi.aoptest.DataManagerImpl.*(..))")
    public void after() {
        System.out.println("after");
    }

    @Around("execution(* com.woitumi.aoptest.DataManagerImpl.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前");
        System.out.println("签名:" + joinPoint.getSignature());
        //执行目标方法proceed
        Object proceed = joinPoint.proceed();
        System.out.println("环绕后");
        System.out.println(proceed);
    }
}
 
  

Spring 学习笔记_第26张图片
5.3.3_OUTPUT.png

6、整合MyBatis

(后续补充)

7、声明式事务

(后续补充)

你可能感兴趣的:(Spring 学习笔记)