【Spring笔记、使用教程】

文章目录

一、SSM介绍

SSM全称Spring+SpringMVC+MyBatis,是继SSH之后,目前比较主流的Java EE企业级框架,适用于搭建各种大型的企业级应用系统。


二、Spring概述

1、介绍

Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。

Spring 的核心技术是控制反转(IOC)和面向切面编程(AOP)。

Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模 块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。

Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。Ioc 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”,注入即赋值。 而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成 “织入”。


2、优点

Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象, 容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。

(1)轻量

Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需 的 jar 总共在 3M 左右。 Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar

(2)针对接口编程,解耦合

Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的 对象创建方式,现在由容器完成。对象之间的依赖解耦合。

(3)AOP 编程的支持

通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现 的功能可以通过 AOP 轻松应付 在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地 进行事务的管理,提高开发效率和质量。

(4)方便集成各种优秀框架

Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring 提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。 Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把 这个插头放入插线板。不需要可以轻易的移除。


三、IOC控制反转

1、概述

控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。

控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值, 依赖的管理。

IoC 的实现方式多种多样。当前比较流行的实现方式是依赖注入:DI(Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。Spring 框架就是使用依赖注入(DI)实现 IoC。Spring底层创建对象使用的是反射。

依赖:classA 类中含有 classB 的实例,在 classA 中调用 classB 的方法完成功能,即 classA 对 classB 有依赖。

依赖注入 DI(Dependency Injection) 是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。

Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称 为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式 来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。

反转控制——IOC(Inverse Of Control)

  1. 创建对象的方式反转
  2. 以前开发由开发人员自己维护,包括依赖关系也是自己注入.
  3. 使用Spring之后对象的创建以及依赖关系可以由Spring完成创建以及注入.
  4. 反转控制就是反转了对象的创建方式,从我们自身创建反转给了程序(Spring)

IOC的体现:

使用Servlet的步骤:

  1. 创建类继承Httpservelt
  2. 在web.xml注册servlet,或者使用注解注册Servlet

在这个过程中没有创建Servlet对象,这里Servlet 是Tomcat服务器它能你创建的。所以Tomcat也称为容器。而Tomcat作为容器:里面存放的有Servlet对象,Listener对象,Filter对象。


2、Spring基本使用

(1)添加Spring依赖

使用最基础的 Spring 得先导包:Beans、Core、Context、SpEL。但是由于Maven项目的依赖传递特性,我们只需要导入一个包即可(它会自动的把其他需要的包导入进来)。

我这里导入的是SpringContext的4.3.14版本

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.14.RELEASE</version>
</dependency>

(2)创建Spring配置文件

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="employees" class="com.study.entity.Employees"/>
</beans>
<!--
1. beans是根标签,spring把Java对象称为bean。
2. spring-beans.xsd是约束文件,和mybatis的 .dtd是一样的。
3. bean标签:告诉spring需要创建某个类的对象
    id:对象的自定义名称,唯一值。spring通过这个名称找到对象
    class:类的全限定名称(不能是接口,因为spring是反射机制创建对象,所以必须使用类
    原理:spring通过该标签就会创建指定类的对象,然后放入到spring框架中存放对象的map集合中
        springMap.put(id值,对象);
-->
Spring配置详解

bean元素:使用该元素描述需要Spring容器管理的对象

  • class属性:被管理对象的完整类型.

  • name属性:给被管理的对象起个名字。获得对象时根据该名称获得对象,可以重复,可以使用特殊字符

  • id属性:与name属性一模一样。名称不可重复, 不能使用特殊字符

    结论:尽量使用name属性

  • Scop属性:作用域。(6种)

    1. singleton(默认值):单例对象。被标识为单例的对象在Spring容器中只会存在一个实例
    2. prototype:多例原型。被标识为多例的对象,每次获得才会创建。每次创建都是新的对象。整合struts2时,ActionBean必须配置为多例的
  • 生命周期属性:配置一个方法作为生命周期初始化方法.Spring会在对象创建之后立即调用 配置一个方法作为声明周期的销毁方法,Spring容器在关闭并销毁所有容器中的对象之前调用

  1. 在user中添加两个方法:

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public class User {
    	private String uNo;
    	private String uName;
    	private String pwd;
    
    	public void init() {
    		System.out.println("初始化");
    	}
    
    	public void destroy() {
    		System.out.println("销毁");
    	}
    }
    
  2. 在applicationContext.xml中添加init-method属性和destroy-method属性。

    <?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">
    
    	<bean class="com.test.User" name="user" init-method="init" destroy-method="destroy"></bean>
    </beans>
    
bean标签中id和name的区别

id和name本质上其实是相同的,都可以唯一地标识一个bean。区别是id只能定义一个值,name可以定义多个值。

  1. 配置一个bean的时候,可以不设置id,也可以不设置name,spring默认会使用类的全限定名作为bean的标识符。
  2. 如果设置id属性,那么id就是bean的唯一标识符,在spring容器中必需唯一。
  3. 如果仅设置name属性,那么name就是bean的唯一标识符,必需在容器中唯一。
  4. 如果同时设置id和name,那么id是唯一标识符,name是别名。如果id和name的值相同,那么spring容器会自动检测并消除冲突:让这个bean只有标识符,而没有别名。
  5. name属性设置多个值且不设置id属性,那么第一个被用作标识符,其他的被视为别名。如果设置了id,那么name的所有值都是别名。
  6. 不管是标识符,还是别名,在容器中必需唯一。因为标识符和别名,都是可以用来获取bean的,如果不唯一,显然不知道获取的到底是哪儿一个bean。

(3)创建类并使用

Employees.java

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employees {
    private Integer employeeNumber;
    private String lastName;
    private String firstName;
    private String extension;
    private String email;
    private String officeCode;
    private Integer reportsTo;
    private String jobTitle;
}

TestSpring.java

import com.study.entity.Employees;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestSpring {
    @Test
    public void springCreatObject() {
        // 该语句执行时配置文件中的所有的对象都将会被创建,默认使用的是无参构造方法
        // ApplicationContextL:spring中的容器
        // ClassPathXmlApplicationContext:从类路径中加载spring配置文件
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        Employees employees = (Employees) ac.getBean("employees");
        employees.setEmployeeNumber(1002);
        employees.setEmail("123456789@qq.com");
        System.out.println(employees.toString());
    }
}

运行结果

二月 19, 2022 11:30:53 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2aae9190: startup date [Sat Feb 19 23:30:53 CST 2022]; root of context hierarchy
二月 19, 2022 11:30:53 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext.xml]
Employees(employeeNumber=1002, lastName=null, firstName=null, extension=null, email=123456789@qq.com, officeCode=null, reportsTo=null, jobTitle=null)

什么时候把对象放入容器中?

什么样的对象放入容器中?

  • dao类、service类、controller类、工具类

    实现:①基于xml的DI ②基于注解的DI

  • spring的对象默认是单例的

什么样的对象不放入容器中?

  • 实体类对象,因为数据都在数据库中
  • servlet、filter、listener等

3、获取spring容器中Java对象的信息

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestSpring {
    @Test
    public void springCreatObject() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 获取spring容器中对象的个数
        int beanDefinitionCount = ac.getBeanDefinitionCount();
        // 获取spring容器中所有对象的名称
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        System.out.println(beanDefinitionCount);
        for (String name : beanDefinitionNames) {
            System.out.println(name);
        }
    }
}

4、依赖注入 DI

DI是IOC实现的重要技术,有如下2种方式:

  1. set方式注入
  2. 构造方式注入

注入类型有如下几种:简单值、集合、Bean对象

1、实现方式一:基于XML的DI

1、set注入

在applicationContext文件中的bean标签中加入property子标签进行赋值。这种注入方式使用的是类的set方法。

简单类型注入
<?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 class="com.study.entity.Employees" id="employees">
        <!--name:属性名;value:属性值-->
        <property name="email" value="123456789@qq.com"/>
        <property name="employeeNumber" value="1002"/>
        <property name="firstName" value=""/>
    </bean>
</beans>
引用类型注入
<bean class="com.study.entity.TestEntity" id="testEntity">
    <property name="testName" value="测试注入引用类型"/>

    <!--引用类型注入。 name:属性名称;ref:值必须为某bean的id -->
    <property name="employees" ref="employees"/>
</bean>

2、构造注入

在bean标签中使用constructor-arg子标签进行赋值。这种方式使用的是类的构造方法进行赋值的。

<bean class="com.study.entity.TestEntity" id="testEntity">
    <!--
		一个constructor-arg代表着构造方法的一个属性。
			name:构造方法形参名
			index:构造方法参数的位置,参数从左往右位置为0,1,2,3,....。如果代码是按照顺序来的,也可以省略index。
				name和index使用一个就行
			value:简单类型的值赋值
			ref:引用类型的值赋值
	-->
    <constructor-arg name="testName" index="0" value="测试注入引用类型"/>
    <constructor-arg name="employees" index="1" ref="employees"/>
</bean>
3、引用类型自动注入

引用类型的自动注入:spring框架根据某些规则自动给引用类型赋值。

常用的使用规则:

  1. byName(按名称注入)
  2. byType(按类型注入)
byName

以往我们想给类中的引用类型赋值是手动注入的:

<bean class="com.study.entity.Employees" id="employees">
    <property name="email" value="123456789@qq.com"/>
    <property name="employeeNumber" value="1002"/>
    <property name="firstName" value=""/>
</bean>

<bean class="com.study.entity.TestEntity" id="testEntity">
    <property name="testName" value="测试注入引用类型"/>

    <!--引用类型手动注入-->
    <property name="employees" ref="employees"/>
</bean>

byname的方式就是在bean标签中使用autowire属性自动给引用类型赋值。

  1. 类中的引用类型的属性名和spring配置文件中的id必须一致
  2. 数据类型一致
<bean class="com.study.entity.Employees" id="employees">
    <property name="email" value="123456789@qq.com"/>
    <property name="employeeNumber" value="1002"/>
    <property name="firstName" value=""/>
</bean>

<!--使用autowire属性自动注入引用类型 -->
<bean class="com.study.entity.TestEntity" id="testEntity" autowire="byName">
    <property name="testName" value="测试注入引用类型"/>
</bean>
byType
  1. 类中的引用类型的类型和spring配置文件中的class是同源的
  2. 同源:同一类型、父子类关系、实现类关系(接口)
  3. spring配置文件中满足同源条件的bean只能有一个,如果大于两个就会报错
<bean class="com.study.entity.Employees" id="employees">
    <property name="email" value="123456789@qq.com"/>
    <property name="employeeNumber" value="1002"/>
    <property name="firstName" value=""/>
</bean>

<bean class="com.study.entity.TestEntity" id="testEntity" autowire="byType">
    <property name="testName" value="测试注入引用类型"/>
</bean>

2、实现方式二:基于注解的DI

使用注解必须依赖于spring-aop,但是我们在maven中加入spring-context的时候,由于Maven项目的依赖传递特性,spring-aop就已经自动导入了,所以这里我们不需要重复导入。

常用注解
常用注解说明备注
@Component创建对象的,相当于bean标签
@Repository用于持久层的注解。放在dao实现类的上面,表示创建dao对象,dao对象是能访问数据库的。
@Service用于业务层的注解,放在service实现类的上面,创建service对象,service对象是做业务处理的,可以有事务功能。
@Controller用于控制器的,放在控制器类的上面,创建控制器对象的,控制器对象能够接受用户提交的参数,显示请求的处理结果。@Repository@Service@Controller三个都是创建对象的,但是却又不同,他们都有自己额外的功能,使用不同的标签是用于分层的。
@Value
@Autowired
@Qualifier
@Resource
使用步骤
  1. 加入spring-aop依赖(maven已经帮我们做了)
  2. 在类中使用spring注解
  3. 在配置文件中,加入组件扫描器标签,说明注解在项目中的位置
1、定义Bean的注解@Component

@Component(value = “对象id”):创建对象的,等同于<bean>标签的功能。

参数:

  • value:对象的名称,相当于bean中的id。value的值时唯一的,创建的对象在整个spring中只有一个。
三种使用方式说明
@Component(value = “对象id”)
@Component(“对象id”)和第一种没有什么区别,只不过省略了value
@Component使用spring默认的命名,类名首字母小写
1、在类中加入注解
@Component(value = "employees")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employees {
    private Integer employeeNumber;
    private String lastName;
    private String firstName;
    private String extension;
    private String email;
    private String officeCode;
    private Integer reportsTo;
    private String jobTitle;
}
2、在spring配置文件中加入组件扫描器标签

component-scan:声明组件扫描器;

组件:就是Java对象;

工作方式:spring会扫描base-package指定的包和子包中的所有类,找到其中的注解,按照注解的功能创建对象或属性赋值。

指定多个包的三种方式:

  1. 使用多次组件扫描器;
  2. 使用分隔符;或,分隔包名;
  3. 指定父包。
<?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.study.entity"/>
</beans>
3、使用
@Test
public void springCreatObject() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    Employees employees = (Employees) ac.getBean("employees");
    System.out.println(employees.toString());
}

2、简单类型属性注入@Value

属性:value,string类型的,表示简单类型的属性值。同样这里也可以省略

位置:

  1. 在属性定义的上面,这种方式无需set方法。推荐使用。
  2. 在set方法的上面。
@Component("employees")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employees {

    @Value(value = "1002")
    private Integer employeeNumber;

    @Value(value = "三")
    private String lastName;

    @Value("张")
    private String firstName;
    private String extension;

    @Value("123456789@qq.com")
    private String email;
    private String officeCode;
    private Integer reportsTo;
    private String jobTitle;
}

3、byType自动注入@Autowired

实现引用类型赋值的注解,默认使用的是byType(根据类型)自动注入。

属性:

  • required:Boolean类型,默认true,可以不写。

    true:引用类型赋值失败,程序报错终止执行;(推荐使用)

    false:引用类型赋值失败,程序继续执行,引用类型为null。

位置:

  1. 在属性定义的上面,这种方式无需set方法。推荐使用。
  2. 在set方法的上面。
@Component
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class TestEntity {
    @Value("测试引用类型使用byType自动注入")
    private String testName;

    @Autowired
    private Employees employees;
}

4、byName自动注入@Autowired与@Qualifier

如果给引用类型注入值要使用byName的方式,就得在使用@Autowired注解的同时加上@Qualifier(value = “bean的id”)。

@Component
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class TestEntity {
    @Value("测试引用类型使用byName自动注入")
    private String testName;

    @Autowired
    @Qualifier(value = "employees")
    private Employees employees;
}

5、JDK注解@Resource自动注入

来自JDK中的注解,spring框架中提供了对这个注解的支持,可以使用该注解给引用类型赋值。默认是byName。

执行流程:

  • 如果没有设置name属性的值,则默认使用byName,如果使用byName失败,则会再使用byType进行赋值。
  • 如果设置了name属性,则只会使用byName的方式赋值。
@Component
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class TestEntity {
    @Value("测试引用类型使用byName自动注入")
    private String testName;

    @Resource
    @Qualifier(value = "employees")
    private Employees employees;
}

3、为项目指定个多个配置文件

如果项目比较大,则一个模块对应一个配置文件 或者 按类的功能分类分别对应一个配置文件,不要一整个项目就一个配置文件。

然后用一个主配置文件将所有小配置文件组合起来。

<!--这是主配置文件-->
<?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">
	
    <!--
		使用import标签导入其他的配置文件,著配置文件一般是不定义对象的。
		类路径:整个项目编译成class文件后,文件所在的路径
	-->
    <import resource="classpath:类路径"/>
    
    <!--
		还可以使用通配符 * ,来一次性加入所有的配置文件
		注意:使用通配符时主文件不能包含在通配符的包含范围内。
	-->
    <import resource="classpath:forEntity/spring-*.xml"/>
</beans>

4、使用配置文件 + ${} 赋值

1、在resource中创建一个文件data.properties

employeeNumber=1002
lastName=三
firstName=张
email=123456789@qq.com

2、在applicationContext中添加<context:property-placeholder />标签

<?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.study.entity"/>

    <context:property-placeholder location="data.properties"  file-encoding="utf-8"/>
</beans>

3、在使用注解赋值时使用${}使用文件定义的变量

@Component("employees")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employees {

    @Value("${employeeNumber}")
    private Integer employeeNumber;

    @Value("${lastName}")
    private String lastName;

    @Value("${firstName}")
    private String firstName;
    private String extension;

    @Value("${email}")
    private String email;
    private String officeCode;
    private Integer reportsTo;
    private String jobTitle;
}

5、可能会出现的编码问题

当你使用配置文件赋值的时候,可能会出现乱码,可以查看编辑页面右下角编码格式可能为GBK,而我们使用的是utf-8,所以运行代码时结果会出现乱码。

解决:在IDEA中设置编码全部为utf-8即可。打开IDEA设置——>编辑器——>文本编码:

在这里插入图片描述

为了方便我们下次创建新项目时不用再次设置,我们点进其他设置——>设置for new Project…——>依照上面的步骤再次设置一遍。


6、xml配置文件和注解的对比

注解优点是:

  • 方便
  • 直观
  • 高效(代码少,没有配置文件的书写那么复杂)。

其弊端也显而易见:以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。

XML 方式优点是:

  • 配置和代码是分离的
  • 在 xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。

xml 的缺点是:编写麻烦,效率低,大型项目过于复杂。

总结:如果不需要经常改动的,就使用注解;如果需要经常改动的,就是用xml配置文件


四、AOP面向切面编程

1、概述

AOP(Aspect Orient Programming):面向切面编程,基于动态代理的,可以使用jdk、cglib两种代理方式。AOP就是动态代理的规范化,把动态代理的实现步骤、方式都定义好了,让开发人员用一种统一的方式使用动态代理。

2、常见术语

常见术语说明
Aspect切面,给你的目标类增加的功能。切面一般都是非业务方法,即是可以独立使用的。常见的切面有:日志、事务、统计信息、参数检查、权限验证等等
JoinPoint连接点,连接业务方法和切面的位置。即类中的业务方法
PointCut切入点,指多个连接点方法的集合
目标对象给哪个类的方法增加功能,这个类就叫做目标对象
Advice通知,即切面功能执行的时间

切面三要素:

  1. 切面的功能代码,切面要干什么?
  2. PointOut,切面的执行位置
  3. Advice,切面的执行时间

3、AOP的实现

AOP是一个规范,是动态的一个规范化,一个标准。

aop的技术实现框架:

  1. spring:spring在内部实现了aop规范,能做aop的工作。
    spring主要在事务处理时使用aop。
    我们项目开发中很少使用spring的aop实现。因为spring的aop比较笨重。

  2. AspectJ:一个开源的专门做aop的框架。spifing框架中集成了aspectj框架,通过spring就能使用AspectJ的功能。AspectJ框架实现AOP有两种方式:

    1. 使用XML配置文件;
    2. 使用注解。(一般项目中我们都是用注解的方式)

4、AspectJ框架实现AOP

1、通知注解

1、常用注解
通知注解属性说明特点
@Aspect加在类上,说明该类是切面类,类中的方法都是切面方法。
@Beforevalue:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)前置通知1. 在目标方法之前执行
2. 不会改变目标方法的执行结果
3. 不会影响目标方法的执行
@AfterReturning1. value:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)
2. returning:自定义的变量,名称必须和通知方法的形参名一样,即用通知方法的形参名接收目标方法的返回值
后置通知1. 在目标方法之后执行的。
2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
3. 可以修改这个返回值
@Aroundvalue:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)环绕通知1. 它是功能最强的通知
2. 在目标方法的前和后都能增强功能。
3. 可以控制目标方法是否被调用执行
4. 修改原来的目标方法的执行结果。影响最后的调用结果
@AfterThrowing1. value:切入点表达式
2. throwing:表示目标方法抛出的异常对象。变量名必须和方法通知方法的形参名一样
异常通知1. 在目标方法抛出异常时执行
2. 可以做异常的监控程序,监控目标方法执行时是不是有异常。如果有异常,可以发送邮件,短信进行通知
@Aftervalue:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)最终通知1. 总是会执行
2. 在目标方法之后执行的
@Pointcutvalue:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)当使用@Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。
其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
2、@Before

前置通知。在目标方法前执行。

通知属性:

  • value:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)

方法参数:

  • JoinPoint:任何通知注解都可以使用,除了环绕通知中不需要使用因为ProceedingJoinPoint是继承值JoinPoint的。该参数可以在通知方法中获取方法执行时的信息,例如方法名称,方法的实参。如果你的切面功能中需要用到方法的信息,就加入JoinPoint。这个JoinPoint参数的值是由框架赋予,必须是第一个位置的参数。

    @Before(value = "execution(public void com.study.service.AopServiceImpl.sayHello(String,int))")
    public void beforeNotice(JoinPoint jp) {
        System.out.println("前置通知:" + new Date());
    
        // 获取方法的签名
        System.out.println(jp.getSignature());
    
        // 获取方法的名称
        System.out.println(jp.getSignature().getName());
    
        // 获取方法的实参
        Object[] args = jp.getArgs();
        for (Object temp : args) {
            System.out.println(temp);
        }
    }
    

具体使用在实现步骤的5中有

3、@AfterReturning

后置通知。在目标方法后执行。

通知属性:

  1. value:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)
  2. returning:自定义的变量,名称必须和通知方法的形参名一样,即用通知方法的形参名接收目标方法的返回值

方法参数:

  • JoinPoint:选择性使用,使用时必须为第一个参数。

  • 任意类型的参数,建议使用Object。用于接收目标方法的返回值。

具体使用在实现步骤的5中有

4、@Around

环绕通知,等同于jdk动态代理的,InvocationHandler接口。环绕通知经常用于做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务

通知属性:

  • value:切入点表达式,表示切面的功能执行的位置(如果只使用该参数则可以省略,直接写值)

方法参数:

  • ProceedingJoinPoint(必须) :等同于invoke方法中的 Method 参数,用于执行目标方法。

方法返回值:

  • 任意类型返回值(必须):建议使用Object。回想动态代理实现InvocationHandler接口,里面的Object invoke(Object proxy,Method method,Object[] args)方法的返回值就是Object类型的,用于输出增强后的结果。

具体使用在实现步骤的5中有

5、@AfterThrowing

异常通知。在目标方法抛出异常时执行。

通知属性:

  1. value:切入点表达式
  2. throwing:表示目标方法抛出的异常对象。变量名必须和方法通知方法的形参名一样

方法属性:

  1. Exception:用于接收异常对象,和通知属性中的throwing参数名相对应。
  2. JoinPoint:选择性使用,使用时必须为第一个参数。

具体使用在实现步骤的5中有

6、@After

最终通知。类似于try catch中的finally一样,总是会在最后执行。一般用于做资源清除工作

通知属性:

  • value:切入点表达式

方法属性:

  • JoinPoint:选择性使用,使用时必须为第一个参数。

具体使用在实现步骤的5中有

7、@Pointut

定义和管理切入点。如果项目中有多个切入点表达式重复了,则可以使用该注解给切入点取别名,方便使用。

注解属性:

  • value:切入点表达式

具体使用在实现步骤的5中有


2、切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。语法:

execution(访问权限? 方法返回值 包名类名?方法声明(参数) 异常类型?)

说明:

  1. ?表示可选的部分

  2. 常用匹配符:

    符号说明
    *0个或者多个任意字符
    用在方法参数中,表示任意多个参数
    用在包名后,表示当前包及其子包路径
    +用在类名后,表示当前类及其子类
    用在接口后,表示当前接口及其实现类

使用示例:

  • 掌握

    execution(public * *(..)) 
    指定切入点为:任意公共方法。
    
    execution(* set*(..)) 
    指定切入点为:任何一个以“set”开始的方法。
    
    execution(* com.xyz.service.*.*(..)) 
    指定切入点为:定义在 service 包里的任意类的任意方法。
    
    execution(* com.xyz.service..*.*(..))
    指定切入点为:定义在 service 包或者子包里的任意类的任意方法。
        “..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
    
    execution(* *..service.*.*(..))
    指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
    
  • 熟悉

    execution(* *.service.*.*(..))
    指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点
    
    execution(* *.ISomeService.*(..))
    指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点
    
    execution(* *..ISomeService.*(..))
    指定所有包下的 ISomeSerivce 接口中所有方法为切入点
    
    execution(* com.xyz.service.IAccountService.*(..)) 
    指定切入点为:IAccountService 接口中的任意方法。
    
    execution(* com.xyz.service.IAccountService+.*(..)) 
    指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意
    方法;若为类,则为该类及其子类中的任意方法。
    
    execution(* joke(String,int)))
    指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参
    数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用
    全限定类名,如 joke( java.util.List, int)execution(* joke(String,*))) 
    指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型,
    如joke(String s1,String s2)joke(String s1,double d2)都是,
    但joke(String s1,double d2,String s3)不是。
    
    execution(* joke(String,..))) 
    指定切入点为:所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且参数类型不限,
    如 joke(String s1)joke(String s1,String s2)joke(String s1,double d2,String s3)
    都是。
    
    execution(* joke(Object))
    指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。
    joke(Object ob)是,但,joke(String s)joke(User u)均不是。
    
    execution(* joke(Object+))) 
    指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。
    不仅 joke(Object ob)是,joke(String s)joke(User u)也是。
    

3、实现步骤

1、加入AspectJ依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.14.RELEASE</version>
</dependency>
2、创建目标类

创建目标类:接口及实现类。(因为我们要做的就是给类中的方法增加功能)

public interface AopService {
    // 测试前置通知
    void sayHelloToBefore(String name);

    // 测试后置通知
    String sayHelloToAfterReturning(String name);

    // 测试环绕通知
    String sayHelloToAround(String name);

    // 测试
    void sayHelloToAfterThrowing(String name);

    // 测试
    void sayHelloToAfter(String name);
}
public class AopServiceImpl implements AopService {
    @Override
    public void sayHelloToBefore(String name) {
        System.out.println("Hello ,Aop I'm " + name);
    }

    @Override
    public String sayHelloToAfterReturning(String name) {
        System.out.println("Hello ,Aop I'm " + name);
        return name;
    }

    @Override
    public String sayHelloToAround(String name) {
        System.out.println("Hello ,Aop I'm " + name);
        return name;
    }

    @Override
    public void sayHelloToAfterThrowing(String name) {
        System.out.println("Hello ,Aop I'm " + name);
        int exception = 10 / 0;
    }

    @Override
    public void sayHelloToAfter(String name) {
        System.out.println("Hello ,Aop I'm " + name);
    }
}
3、创建切面类

创建切面类:里面定义的方法就是需要增加的功能代码

  1. 类的上方加上@Aspect注解,表示当前类是切面类
  2. 在方法的上面加上通知注解 和 切入点表达式

注意:切面类中的方法的参数并不是任意指定的,需要遵循AspectJ的规范

@Aspect
public class AopServiceAspect {
    // 使用前置通知
    @Before(value = "execution(public void com.study.service.AopServiceImpl.sayHelloToBefore(String))")
    public void beforeNotice(JoinPoint jp) {
        System.out.println("【前置通知】" + new Date());

        // 使用JoinPoint
        // 获取方法的签名
        System.out.println("【前置通知】方法签名:" + jp.getSignature());
        // 获取方法的名称
        System.out.println("【前置通知】方法名称:" + jp.getSignature().getName());
        // 获取方法的实参
        Object[] args = jp.getArgs();
        for (Object temp : args) {
            System.out.println("【前置通知】方法实参:" + temp);
        }
    }

    // 使用后置通知
    @AfterReturning(value = "execution(public String sayHelloToAfterReturning(..))", returning = "arg")
    public void afterReturningNotice(Object arg) {
        System.out.println("【后置通知】" + new Date());
        System.out.println("【后置通知】返回值为:" + arg);
    }

    /**
     * 使用环绕通知
     * 环绕通知方法的定义中
     * 1. 必须有返回值,建议使用Object
     * 2. 必须有一个固定的参数 ProceedingJoinPoint
     */
    @Around("execution(public String com.study.service.AopServiceImpl.sayHelloToAround(..))")
    public Object aroundNotice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("【环绕通知】" + new Date());

        // 使用JoinPoint的子类ProceedingJoinPoint
        // 获取方法的签名
        System.out.println("【环绕通知】方法签名:" + pjp.getSignature());
        // 获取方法的名称
        System.out.println("【环绕通知】方法名称:" + pjp.getSignature().getName());
        // 获取方法的实参
        Object[] args = pjp.getArgs();
        for (Object temp : args) {
            System.out.println("【环绕通知】方法实参:" + temp);
        }

        // 根据参数符合条件与否,调用目标方法
        Object result = null;
        if (args.length >= 1) {
            // 如果传过来的参数是 环绕通知,则调用目标方法
            if (args[0].equals("环绕通知")) {
                result = pjp.proceed();
            }
        }

        System.out.println("【环绕通知】" + new Date());

        return result;
    }

    // 使用异常通知
    @AfterThrowing(value = "execution(public void sayHelloToAfterThrowing(String))", throwing = "ex")
    public void afterThrowingNotice(Exception ex) {
        System.out.println("【异常通知】" + new Date());

        // 输出异常信息
        System.out.println("【异常通知】" + ex.getMessage());
    }

    // 使用最终通知
    @After("sayHelloToAfter()")
    public void afterNotice() {
        System.out.println("【最终通知】" + "资源清除工作");
    }

    // 使用Pointcut注解
    @Pointcut("execution(public void sayHelloToAfter(String))")
    public void sayHelloToAfter() {
    }
}
4、创建Spring配置文件

创建Spring配置文件:声明对象,把对象交给容器统一管理

  1. 声明目标对象
  2. 声明切面类对象
  3. 声明AspectJ框架中的自动代理生成器标签。即完成代理对象的自动创建。
<?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 class="com.study.service.AopServiceImpl" id="aopService"/>
    <bean class="com.study.AopServiceAspect" id="aopServiceAspect"/>

    <!--
        声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构。
        创建为代理对象,所以目标对象就是被修改后的代理对象。
		该标签会将spring容器中所有的目标对象,一次性都生成代理对象。
    -->
    <aop:aspectj-autoproxy/>
</beans>
5、测试类中获取代理对象并使用
public class TestAop {
    @Test
    public void test() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 这里必须使用接口类型接收
        AopService aopService = (AopService) ac.getBean("aopServiceImpl");

        aopService.sayHelloToBefore("前置通知");
        System.out.println();
        String s = aopService.sayHelloToAfterReturning("后置通知");
        System.out.println("s:" + s);
        System.out.println();
        String s1 = aopService.sayHelloToAround("环绕通知");
        System.out.println("s1:" + s1);
        System.out.println();
        aopService.sayHelloToAfter("最终通知");
        System.out.println();
        aopService.sayHelloToAfterThrowing("异常通知");
    }
}
Java 通过getbean取出的类为什么要强转为接口类?

首先问题是为什么在bean文件中注入的是实现类,但是通过getBean()取出的时候却必须强制转化为接口类?

这个问题应该是和spring中配置的代理模式相关的,即到底是使用JDK动态代理还是Cglib代理。

关于代理模式这个问题spring的文档中这么写的:Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理,如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个Cglib代理。

简单来说,他们之间的区别就是:

  • JDK动态代理的代理对象不需要实现接口,但是目标对象一定要实现接口
  • 如果目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理

使用Cglib代理的时候,通过getBean()取出的注入对象既可以是普通对象,也可以是接口,通过JDK动态代理就只能使用接口。而一般代码习惯中使用JDK动态代理还是更常见的。

为什么通过JDK动态代理就只能使用接口?

如果只是单纯注入是可以用实现类接收注入对象的,但是往往开发中会对实现类做增强,如事务,日志等,实现增强的AOP技术是通过JDK动态代理实现的,而JDK动态代理对实现类对象做增强得到的增强类与实现类是兄弟关系,所以不能用实现类接收增强类对象,只能用接口接收。由于以上原因,如果将对象注入给实现类而非接口的话,在代理时就会报错:java.lang.ClassCastException: com.sun.proxy.$Proxy10 cannot be cast to com.study.service.AopServiceImpl

Cglib代理类和实现类之间是父子关系,自然可以用实现类去接收代理类对象,但是一般这样做是没有意义的。

通常来说,由于java是面向对象的开发,JDK动态代理可以避免一些没有实现接口的对象代理,但是开发中有时候也会实现一些对象的注入(例如Util类),而Cglib刚好支持。

让Spring强制使用Cglib代理:

<aop:aspectj-autoproxy proxy-target-class="true"/>

5、AOP作用

  1. 在不修改目标类的前提下,增加功能
  2. 减少重复的代码
  3. 专注业务功能的实现
  4. 解耦合:业务功能和日志、事务这些非业务功能的耦合

6、什么时候使用AOP技术

  1. 当你需要修改一个类的功能,而又不能直接修改代码的情况下就得使用AOP
  2. 当你需要给很多类添加一个相同的功能时使用AOP
  3. 给业务方法增加事务、日志输出

五、Spring集成MyBatis

利用IOC把MyBatis框架中的对象交给spring统一创建,开发人员从spring中获取对象。这样开发人员就不用面对多个框架进行开发了。

由于MyBatis中的数据库连接池太弱了,所以我们使用另外的独立的数据库连接池代替它。即主配置文件中不需要配置数据库了。

1、MyBatis使用步骤

在这里插入图片描述

MyBatis笔记

  1. 导包:MyBatis、MySQL、LomBok
  2. 创建Pojo类,也叫实体类
  3. 配置文件mapper、SqlMapConfig、db.properties
  4. 创建Dao接口
  5. 使用

2、Spring整合MyBatis步骤

在这里插入图片描述

  1. 加入maven依赖:Spring、MyBatis、MySQL、LomBok、Spring事务的依赖、MyBatis和Spring集成的依赖(MyBatis官方的,用于创建SqlSessionFactory、dao对象的)
  2. 创建Pojo类,也叫实体类
  3. 创建Dao接口和mapper配置文件
  4. 创建SqlMapConfig(MyBatis主配置文件)、db.properties(数据库连接信息)配置文件
  5. 创建Service接口和实现类
  6. 创建Spring配置文件:声明MyBatis的对象交给Spring创建(Druid连接池对象、SqlSessionFactory对象、dao对象)
  7. 测试使用

注意:Spring和MyBatis整合在一起使用时,事务是自动提交的,无需执行SqlSession.commit();

1、加入Maven依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.study</groupId>
    <artifactId>study_ssm</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <!-- MyBatis依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.1</version>
        </dependency>

        <!-- MyBatis和Spring集成的依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

        <!-- 自动生成get、set方法的依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>

        <!-- MySQL依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <!-- Spring IOC依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>

        <!-- Spring事务依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.3.14.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.14.RELEASE</version>
        </dependency>

        <!-- 阿里数据库连接池的依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>

        <!-- Spring AspectJ依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <!--资源文件(非Java文件)所在的目录-->
                <directory>src/main/java</directory>
                <includes>
                    <!--包括目录下的.properties和.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <!--filtering 选项 false 不启用过滤器, *.property 已经起到过滤的作用了 -->
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>

2、创建Pojo类

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employees {
    private Integer employeeNumber;
    private String lastName;
    private String firstName;
    private String extension;
    private String email;
    private String officeCode;
    private Integer reportsTo;
    private String jobTitle;
}

3、创建Dao接口和对应的Mapper文件

dao接口

public interface EmployeesDao {
    // 查询所有的员工信息
    List<Employees> selectEmployees();
}

对应的mapper文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.dao.EmployeesDao">
    <select id="selectEmployees" resultType="Employees">
        select * from employees
    </select>
</mapper>

4、创建MyBatis主配置文件 + db.properties

<?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>
    <settings>
        <!-- 设置mybatis输出日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!-- 取别名 -->
    <typeAliases>
        <package name="com.study.entity"/>
    </typeAliases>

    <!-- 这里不需要配置mysql环境 -->

    <!-- 映射器 -->
    <mappers>
        <mapper resource="mapper_employees.xml"/>
    </mappers>
</configuration>

db.properties

jdbc.url=jdbc:mysql://127.0.0.1:3306/mysqldemo?serverTimezone=UTC&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=password
jdbc.maxActive=20

5、创建Service接口和实现类

Service包里面都是业务层的操作。

Service接口

public interface EmployeesService {
    List<Employees> queryEmployees();
}

Service接口实现类

public class EmployeesServiceImpl implements EmployeesService {
    private EmployeesDao employeesDao;

    // 这里写set方法的目的是为了使用Spring的set注入赋值
    public void setEmployeesDao(EmployeesDao employeesDao) {
        this.employeesDao = employeesDao;
    }

    @Override
    public List<Employees> queryEmployees() {
        List<Employees> employees = employeesDao.selectEmployees();
        return employees;
    }
}

6、创建Spring配置文件

  1. DataSource数据源
  2. SqlSessionFactory对象
  3. Dao对象
  4. Service对象
<?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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 添加外部配置文件 -->
    <context:property-placeholder location="classpath:db.properties" file-encoding="utf-8"/>

    <!-- 1、数据源DataSource配置:即配置数据库 -->
    <!-- init-method:初始方法;destroy-method:销毁方法 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <property name="filters" value="stat"/>

        <!-- 设置连接池最大容量 -->
        <property name="maxActive" value="${jdbc.maxActive}"/>
        <!-- 设置初始创建连接数 -->
        <property name="initialSize" value="1"/>
        <property name="maxWait" value="6000"/>
        <property name="minIdle" value="1"/>

        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <property name="poolPreparedStatements" value="true"/>
        <property name="maxOpenPreparedStatements" value="20"/>

        <property name="asyncInit" value="true"/>
    </bean>

    <!-- 2、SqlSessionFactory -->
    <!--
        使用的是MyBatis和Spring集成的依赖中的SqlSessionFactoryBean类
		来创建SqlSessionFactory对象的
    -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 配置数据库连接池的信息,引用类型注入,值用ref给 -->
        <property name="dataSource" ref="dataSource"/>
        <!--
            配置MyBatis主配置文件的信息
            configLocation的类型是Resource,该类是Spring中用于读取配置文件的
            值是外部文件路径,所以使用value+classpath
        -->
        <property name="configLocation" value="classpath:SqlMapConfig.xml"/>
    </bean>

    <!-- 3、Dao对象 -->
    <!--
        之前创建Dao对象是通过SqlSession的getMapper(EmployeesDao.class)创建的
        而这里我们使用的是MyBatis和Spring集成的依赖中的MapperScannerConfigurer来创建的
        MapperScannerConfigurer:在内部条用getMapper()生成每个dao接口的代理对象
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--
            指定sqlSessionFactory
            由于sqlSessionFactoryBeanName是String类型的,所以使用value属性赋值
        -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

        <!--
            指定mapper接口
            原理:将dao接口所在包名传过去,MapperScannerConfigurer会扫描这个包中所有的接口,
                    把每个接口都执行一次getMapper()方法,得到每个接口的dao对象。
                    创建好的对象放到Spring的容器中。dao对象的默认名称是 接口名首字母小写。
        -->
        <property name="basePackage" value="com.study.dao"/>
    </bean>
    
    <!-- 4、Service:给Service的实现类进行依赖注入 DI,因为Service里面会用到前面创建的dao对象实现业务功能 -->
    <bean class="com.study.service.EmployeesServiceImpl" id="employeesService">
        <!-- 这里ref的值是3中为我们创建的dao对象 -->
        <property name="employeesDao" ref="employeesDao"/>
    </bean>
    
    <!--
        声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构。
        创建为代理对象,所以目标对象就是被修改后的代理对象。
    -->
    <aop:aspectj-autoproxy/>
</beans>

7、测试使用

@Test
public void test() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    // 获取Spring容器中所有对象的信息
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String name : beanDefinitionNames) {
        System.out.println("【spring容器中类信息】" + name);
    }

    // 通过获取Dao对象操作数据库
    EmployeesDao employeesDao = (EmployeesDao) ac.getBean("employeesDao");
    List<Employees> employees = employeesDao.selectEmployees();
    for (Employees e : employees) {
        System.out.println("【使用dao对象】" + e.toString());
    }

    // 通过获取Service对象操作数据库
    // 一般我们不会直接使用dao对象,而是通过service对象操作数据库,在service中使用dao对象
    EmployeesService employeesService = (EmployeesService) ac.getBean("employeesService");
    List<Employees> employees1 = employeesService.queryEmployees();
    for (Employees e : employees1) {
        System.out.println("【使用service对象】" + e.toString());
    }
}

六、Spring事务

1、什么是事务?

事务是指一组sql语句的集合,集合中可能有insert、update、select、delete,这些sql语句的执行是一致的,作为一个整体执行,要么一起成功,要么一起失败。

2、什么时候使用事务?

当我们的操作涉及到多个表、多条sql语句(insert、update、delete)。需要保证数据的一致性,即这些sql语句全部成功执行或者全部失败执行。

3、事务放在哪?

service类的业务方法上,因为业务方法会调用多个dao方法,执行多个sql语句。

4、使用JDBC、MyBatis访问数据库处理事务有什么不足?

JDBC处理事务:Connection conn;

  • conn.commit(); 提交事务

  • conn.rollback(); 回滚事务

MyBdtis处理事务:

  • sqlSession.commit(); 提交事务
  • sqlSession.rollback(); 回滚事务

hibernate处理事务:

  • Session.commit();
  • Session.rollback();

不足:

  1. 不同的数据库访问技术,处理事务的对象、方法不同;
  2. 对于开发人员来说需要了解掌握不同的数据访问技术使用事务的原理,带来了不便。

5、怎么解决不足?

spring提供一种处理事务的统一模型,能使用统一步骤,方式完成多种不同数据库访问技术的事务处理。

6、使用处理事务步骤

怎么使用Spring处理事务?——使用Spring处理事务的固定步骤即可。

(1)声明事务管理器对象

Spring内部提交事务、回滚事务,使用的是事务管理器对象。事务管理器是PlatfromTransactionManager接口和它的众多实现类。

实现类:

  • MyBatis访问数据库——DataSourceTransactionManage
  • hibernate访问数据库——HibernateTransactionManage

在Spring主配制文件中使用bean标签声明你使用的 数据库访问技术 对应的事务管理器对象即可。

<!-- 数据源DataSource配置 -->
<!-- init-method:初始方法;destroy-method:销毁方法 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>

    <property name="filters" value="stat"/>

    <!-- 设置连接池最大容量 -->
    <property name="maxActive" value="${jdbc.maxActive}"/>
    <!-- 设置初始创建连接数 -->
    <property name="initialSize" value="1"/>
    <property name="maxWait" value="6000"/>
    <property name="minIdle" value="1"/>

    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="minEvictableIdleTimeMillis" value="300000"/>

    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>

    <property name="poolPreparedStatements" value="true"/>
    <property name="maxOpenPreparedStatements" value="20"/>

    <property name="asyncInit" value="true"/>
</bean>

<!--声明事务管理器对象-->
<bean id="mybatis" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--连接数据库,指定数据源信息-->
    <property name="dataSource" ref="dataSource"/>
</bean>

(2)指定那些类、方法需要加入事务的功能

1、注解方案:适合中小型项目

1、开启事务注解驱动:告诉Spring框架我要使用注解的方式管理事务。

<!--
    选http://www.springframework.org/schema/tx的annotation-driven
    transaction-manager
-->
<tx:annotation-driven transaction-manager="mybatis"/>

2、使用@Transactional注解添加事务

Spring使用AOP机制实现给业务方法增加事务功能,创建@Transactional所在类的的代理对象,给方法加入事务的功能。这里Spring使用的是环绕通知:在业务方法开始之前,先开启事务,在业务方法之后提交或回滚事务。

  1. 注解放在方法上:只能放在public方法上,否则Spring不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。
  2. 注解放在类上:表示该类上所有的方法均将在执行时织入事务。

属性:

  1. propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED。
  2. isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。
  3. readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。
  4. timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
  5. rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有 一个异常类时,可以不使用数组。
  6. rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。
  7. noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。
  8. noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.DEFAULT,
    readOnly = false,
    rollbackFor = {NullPointerException.class, Exception.class}
)
public void buy(Integer goodsId, Integer numbers) {...}

// 由于我们一般使用的都是默认值,所以以下代码即可:
@Transactional
public void buy(Integer goodsId, Integer numbers) {...}
2、配置文件方案:适合大型项目

使用AspectJ框架的功能,在Spring配置文件中声明类、方法需要的事务。这种方式业务方法和事务配置是完全分离的。

别忘了加上AspectJ依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

1、声明方法需要的事务类型

配置方法的事务属性:隔离级别、传播行为、超时

<!--
   使用http://www.springframework.org/schema/tx的
   transaction-manager:指定事务管理器对象的ID
-->
<tx:advice id="myAdvice" transaction-manager="mybatis">
    <!-- 配置事务属性 -->
    <tx:attributes>
        <!--
                tx:method:给具体的方法配置事务属性
                name:方法名称,只需给出完整的方法名称即可,不需要带上包名。可以使用通配符:*
                propagation:传播行为,枚举值
                isolation:隔离级别
                no-rollback-for:指定的异常类名,全限定类名
            -->
        <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                   no-rollback-for="java.lang.NullPointerException,java.lang.Exception"/>
    </tx:attributes>
</tx:advice>

2、配置AOP

指定哪些类需要创建代理

<!-- 配置AOP -->
<aop:config>
    <!-- 配置切入点表达式:指定哪些包中的哪些类哪些方法要使用事务 -->
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
    <!-- 配置增强器,关联advice和pointCut -->
    <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
</aop:config>

(3)指定方法需要的隔离级别、传播行为、超时时间

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作

1、事务的隔离级别

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

  1. DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。
  2. READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  3. READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
  4. REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读
  5. SERIALIZABLE:串行化。不存在并发问题。
2、事务传播行为

事务传播行为:处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。

  1. PROPAGATION_REQUIRED

    指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。

  2. PROPAGATION_REQUIRES_NEW

    指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

  3. PROPAGATION_SUPPORTS

    总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

  4. PROPAGATION_MANDATORY

  5. PROPAGATION_NESTED

  6. PROPAGATION_NEVER

  7. PROPAGATION_NOT_SUPPORTED

3、事务默认超时时间

常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。

注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。

7、Spring事务提交、回滚的时机

  1. 当业务方法执行成功,没有抛出异常时,Spring在方法执行完毕后提交事务,调用事务管理器的commit。

  2. 当业务方法抛出运行时异常或ERROR,Spring执行回滚,调用事务管理器的rollback。

    运行时异常:RuntimeException和它的子类都是运行时异常:NullPointException、NumberFormatException

  3. 当业务方法抛出非运行时异常,主要是受查异常时,提交事务。

    受查异常:写代码时必须处理的异常:IOException、SQLException


七、Spring与Web

web项目别忘了加入Servlet、Jsp依赖

<!-- servlet的依赖 -->
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

<!-- jsp的依赖 -->
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/jsp-api -->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.1</version>
    <scope>provided</scope>
</dependency>

回顾我们的JavaEE项目,我们运行项目是执行的main方法,在main方法中创建Spring容器对象。可是现在web项目我们是运行在tomcat服务器上的,tomcat项目一旦启动,项目就会一直运行。而我们的Spring容器对象只需要创建一次,如果多次创建则会产生不必要的内存消耗。

解决思路:

  • 把我们创建好的Spring容器对象放入到全局作用域ServletContext中。

实现:

  • 使用监听器,在项目最开始创建全局作用域ServletContext时,创建Spring容器对象放入其中。

    1. 加入Spring框架中的监听器依赖

      <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>5.2.5.RELEASE</version>
      </dependency>
      
    2. 在web.xml文件中注册监听器

      <?xml version="1.0" encoding="UTF-8"?>
      <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
               version="4.0">
          <!--
              监听器被创建后,会读取/WEB-INF/applicationContext.xml文件
              为什么要读?
                  因为在监听器中需要创建ApplicationContext对象,
                  在创建对象时/WEB-INF/applicationContext.xml是监听器默认读取Spring配置文件的路径。
              解决:
                  修改默认路径。使用context-param标签重新指定文件的位置。
          -->
          <context-param>
              <!-- contextConfigLocation表示配置文件的路径 -->
              <param-name>contextConfigLocation</param-name>
              <!-- 自定义配置文件的路径 -->
              <param-value>classpath:applicationContext.xml</param-value>
          </context-param>
          <!-- 注册监听器 -->
          <listener>
              <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
          </listener>
      </web-app>
      
    3. 在Servlet中获取Spring容器并使用

      @WebServlet(name = "TestServlet")
      public class TestServlet extends HttpServlet {
          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              WebApplicationContext applicationContext = null;
      
              // 获取全局作用域
              ServletContext servletContext = getServletContext();
              // 使用框架中的方法获取Spring容器对象
              applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
          }
      
          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doPost(request, response);
          }
      }
      

Servlet中获取Spring容器原理

源代码中:ContextLoaderListener实现了ServletContextListener(标准的监听器),在它的初始方法中创建了一个WebApplicationContext对象,该对象的实现类继承自ApplicationContext类。即WebApplicationContext是我们在web项目中使用的容器对象,而ApplicationContext是我们在JavaSE项目中使用的容器对象。

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

这是ContextLoaderListener在初始化方法中将Spring容器对象放入web的全局作用域中的代码。key是框架中的一个字符串变量,代表的是WebApplicationContext类的完整名称;value是context的容器对象。

如果不使用框架中的方法获取Spring容器对象,则步骤如下:

@WebServlet(name = "TestServlet")
public class TestServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        WebApplicationContext applicationContext = null;

        // 获取ServletContext中的容器对象
        String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
        Object attribute = getServletContext().getAttribute(key);
        if (attribute != null) {
            applicationContext = (WebApplicationContext) attribute;
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

使用框架获取Spring容器对象的getWebApplicationContext方法的源码中:

return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

sc:web全局作用域

WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE:前面ContextLoaderListener在初始化方法中存入Spring容器对象使用的key。

而方法getWebApplicationContext中的代码就是我们前面 不使用框架中的方法获取Spring容器对象 的代码。

所以框架获取Spring容器对象的底层原理就是我们 不使用框架中的方法获取Spring容器对象 的代码。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BUG_GUB

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值