Spring

Spring简介

听说这玩意是春天,说是可以给JAVA程序员带来的春天,但是我感觉不是那么回事,我感觉确实是个春天,只是我没有看到。

spring的主要作用就是为了代码解耦,降低代码之间的偶是和。让对象和对象之间关系不是使用代码关联,同时通过配置来说明。
Spring的核心控制反转(IOC)和面向切面(AOP),简单来说Spring是一个分层JavaEE一站式轻量级开源框架。

IOC:控制反转/依赖注入,在之前学习的过程中,比如有一个类,我们想要调用类里面的方法,就要创建该类的对象,使用对象调用方法来实现。但是对于Spring来说,不用再自己创建要使用的对象,而是由Spring容器统一管理,自动注入,注入就是赋值
AOP:面向切面编程,简单来说就是我们可以在不修改源码的情况下,对程序的方法进行增强,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,再合适的时机将这些切面横向切入到业务流程的指定的位置中

Spring的体系结构是由7个模块构成的,主要由Data Access/Integration(数据访问/集成)、Web、AOP(面向切面编程)、Core Container(核心容器)、Testing(测试)、Messaging(消息)、Web MVC(Web模型-视图-控制器)等模块组成。

核心容器

核心容器提供了Spring框架的基本功能。它包括IOC容器、Bean工厂、事件发布、资源加载等。

Spring管理Bean

Spring管理Bean就是将对象的创建、销毁、配置和使用等生命周期都交给Spring容器来管理。

管理Bean的方式

管理Bean的方式有三种,分别是基于XML配置文件的方式、基于注解的方式和基于Java配置类的方式。

我们这个阶段主要是使用注解和XML配置文件的方式来管理Bean。
什么是Bean

Bean是Spring管理的对象,它的生命周期由Spring容器来管理。
Bean的生命周期包括创建、初始化、使用和销毁四个阶段
Bean是一个普通的Java对象(POJO)被注册到了Spring的IOC容器中可以被其他的Bean依赖注入。
Bean的作用域包括singleton(单例)、prototype(原型)、request(请求)、session(会话)、global session(全局会话)等。
默认情况下,Bean的作用域是singleton,即单例模式。

接下来我来介绍下Spring Bean的配置过程。

1).创建maven工程并补充以下依赖引入

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>

2).创建Bean的配置文件

我们可以在src/main/resources目录下创建一个applicationContext.xml文件,用于配置Bean。这玩意是死的,请切记不要手打

1
2
3
4
5
6
<?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">

</beans>

如果以上部分不够涵盖大多数情况,我这里提供一个更全面的

1
2
3
4
5
6
7
8
<?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">

</beans>

以上配置文件中,我们引入了context命名空间,用于配置自动扫描Bean的包路径。

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

以上配置表示扫描com.bok包及其子包下的所有类,将标注了@Controller、@Service、@Repository、@Component等注解的类注册为Bean。

注意:

以上配置文件中,我们引入了context命名空间,用于配置自动扫描Bean的包路径。
但是,我们需要在Java代码中获取Bean,所以我们需要在Java代码中创建一个ApplicationContext对象,用于加载配置文件。
我们可以使用ClassPathXmlApplicationContext类来加载配置文件,代码如下:

1
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

以上代码表示加载src/main/resources目录下的applicationContext.xml文件。

我们可以使用getBean()方法来获取Bean,代码如下:

1
UserService userService = (UserService) context.getBean("userService");

以上代码表示获取id为userService的Bean,并将其转换为UserService类型。

我们也可以使用泛型来获取Bean,代码如下:

1
UserService userService = context.getBean(UserService.class);

以上代码表示获取类型为UserService的Bean。

我们可以使用@Autowired注解来自动注入Bean,代码如下:

1
2
@Autowired
private UserService userService;

以上代码表示自动注入id为userService的Bean。

我们也可以使用@Resource注解来自动注入Bean,代码如下:

1
2
@Resource
private UserService userService;

以上代码表示自动注入id为userService的Bean。

我们可以使用@Qualifier注解来指定Bean的名称,代码如下:

1
2
3
@Autowired
@Qualifier("userService")
private UserService userService;

以上代码表示自动注入id为userService的Bean。

我们可以使用@Value注解来注入普通属性,代码如下:

1
2
@Value("${jdbc.driver}")
private String driver;

以上代码表示注入jdbc.driver属性的值。

我们可以使用@PropertySource注解来指定属性文件,代码如下:

1
@PropertySource("classpath:jdbc.properties")

以上代码表示加载classpath下的jdbc.properties文件。

我们可以在属性文件中定义属性,代码如下:

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456

以上代码表示定义了jdbc.driver、jdbc.url、jdbc.username、jdbc.password四个属性。

我们可以使用@Configuration注解来定义配置类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}

以上代码表示定义了一个DataSource Bean,用于连接数据库。

==以上内容请按需配置,除了xml头和beans标签,其他内容根据实际情况配置。==

为Bean属性赋值

我们可以使用@Value注解来为Bean的属性赋值,代码如下:

1
2
@Value("${jdbc.driver}")
private String driver;

以上代码表示为driver属性赋值。

我们也可以使用@Autowired注解来为Bean的属性赋值,代码如下:

1
2
@Autowired
private DataSource dataSource;

以上代码表示为dataSource属性赋值。

我们可以使用@Resource注解来为Bean的属性赋值,代码如下:

1
2
3
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;

以上代码表示为dataSource属性赋值。

我们还可以使用构造器来为Bean的属性赋值(xml配置)

1
2
3
<bean id="userService" class="com.bok.service.UserServiceImpl">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>

以上代码表示为userService Bean的dataSource属性赋值。
注意啊,构造器的参数名要和Bean的属性名一致。且如果没有无参构造函数,无法实例化成功,传入的参数也要对应。

我们还可以使用setter方法来为Bean的属性赋值(xml配置)

1
2
3
<bean id="userService" class="com.bok.service.UserServiceImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>

以上代码表示为userService Bean的dataSource属性赋值。
注意啊,setter方法的参数名要和Bean的属性名一致。

工厂模式

工厂模式是java中最常用的设计模式之一,这种类型的设计模式属性创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会暴露创建逻辑,同时通过使用一个共同的接口来指向新创建的对象。

静态工厂方式创建Bean

首先静态工厂是一个普通的类,它包含一个静态方法,这个方法返回一个对象。
代码如下:

1
2
3
4
5
public class StaticFactory {
public static UserService getUserService() {
return new UserServiceImpl();
}
}

以上代码表示定义了一个静态工厂类,它包含一个静态方法getUserService(),这个方法返回一个UserService对象。
我还可以提供一个更接地气的案例:

1
2
3
4
5
6
7
8
9
public class StaticFactory {

// 模拟静态工厂 ,创建Person实例
public static Person createPerson(String name){
Person p=new Person();
p.setName(name);
return p;
}
}

然后我们可以在xml配置文件中使用静态工厂方式创建Bean,代码如下:
1
2
3
<bean id="person" class="com.bok.factory.StaticFactory" factory-method="createPerson">
<constructor-arg value="张三"></constructor-arg>
</bean>

以上代码表示使用StaticFactory类的createPerson()方法创建一个Person对象,对象的name属性为张三。

示例工厂创建Bean

示例工厂是一个普通的类,它包含一个普通方法,这个方法返回一个对象。
代码如下:

1
2
3
4
5
public class ExampleFactory {
public UserService getUserService() {
return new UserServiceImpl();
}
}

以上代码表示定义了一个示例工厂类,它包含一个普通方法getUserService(),这个方法返回一个UserService对象。
我还可以提供一个更接地气的案例:
1
2
3
4
5
6
7
8
public class ExampleFactory {
// 模拟示例工厂 ,创建Person实例
public Person createPerson(String name){
Person p=new Person();
p.setName(name);
return p;
}
}

以上代码表示定义了一个示例工厂类,它包含一个普通方法createPerson(),这个方法返回一个Person对象。
我们可以在xml配置文件中使用示例工厂方式创建Bean,代码如下:
1
2
3
4
<bean id="exampleFactory" class="com.bok.factory.ExampleFactory"></bean>
<bean id="person" factory-bean="exampleFactory" factory-method="createPerson">
<constructor-arg value="张三"></constructor-arg>
</bean>

以上代码表示使用ExampleFactory类的createPerson()方法创建一个Person对象,对象的name属性为张三。
注意啊,factory-bean属性指定了工厂Bean的名称,factory-method属性指定了工厂方法的名称。

Spring管理连接池

一个项目就一个连接池,连接池里面管理很多连接,连接是直接从连接池中拿,可以让Spring帮我们创建连接池对象。
引入外部文件,数据库的连接信息配置在单独的db.properties文件

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456

以上代码表示定义了jdbc.driver、jdbc.url、jdbc.username、jdbc.password四个属性。

我们可以使用@Configuration注解来定义配置类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@PropertySource("classpath:db.properties")
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}

我们还可以使用Maven依赖引入,这个做法更主流一些,例如spring管理C3P0数据库连接池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>c3p0</groupId>

<artifactId>c3p0</artifactId>

<version>0.9.1.2</version>

</dependency>

<dependency>
<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>5.1.46</version>

</dependency>

如果你在IOC容器中创建了一个数据库连接池对象,那么就需要吧这些数据给加载进来
1
2
3
4
5
6
7
8
9
10
<!--  加载外部的properties属性文件到IOC容器中-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

基于注解的容器配置

在之前的案例中,Spring创建person对象,在容器中使用bean标签进行配置,这是基于XML配置的方式,可以通过bean上添加某些注解,可以快速的将bean加入到IOC容器中。
通过注解分别创建Dao、Service、Controller(控制器,servlet)
上述文件结构树为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src
├── main
│ ├── java
│ │ └── com
│ │ └── bok
│ │ ├── dao
│ │ ├── service
│ │ ├── controller
│ │ └── factory
│ └── resources
│ ├── db.properties
│ └── spring.xml
└── test
├── java
└── resources

在组件上使用注解

在需要被Spring管理的类的使用一下注解:
以下注解在某个类上加入任何一个注解能够快速的将这个组件加入到IOC容器的管理中:
● @Componet:容器管理bean的基础注解,应用于具体的实现类上,不属于以下三类时,用此注解。
@Service:一般应用于业务层(PersonService)
@Repository:一般应用于持久层(PersonDao)
@Controller:一般应用于控制层(PersonServlet)
以上注解可以随便加,Spring底层不会去验证你的组件,但是我们推荐各层用各自的注解。
使用注解将组件快速的加入到容器中需要以下几个步骤:
1.给要添加的类(组件)上标上四个注解的任何一个
2.告诉Spring自动扫描了注解的组件
3.在xml配置文件中添加组件扫描的配置

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

以上代码表示扫描com.bok包及其子包下的所有类,将标注了注解的类加入到IOC容器中。
4.在测试类中获取Bean
1
2
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
PersonService personService = context.getBean(PersonService.class);

以上代码表示从IOC容器中获取PersonService对象。
5.运行测试类
1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
PersonService personService = context.getBean(PersonService.class);
personService.add();
}
}

以上代码表示运行测试类,从IOC容器中获取PersonService对象,调用add()方法。
6.运行结果
1
添加成功

以上代码表示运行结果,说明注解配置成功。

在使用注解的时候没有XML中的id和class属性,如何来识别bean对象的呢?
通过value属于设置当前bean在IOC容器中索引值,即bean标签的id值,默认是当前类对象的首字母小写作为id来识别的。
如果需要更改名称在此注解上加value的参数值即可。
修改PersonDao

1
2
3
4
5
6
/*当只设置value属性时,value可以省略掉 
@Repository("personDao") @Repository
*/
@Repository
public class PersonDao {
}

修改PersonService
1
2
3
4
@Service
public class PersonService {

}

修改PersonServlet
1
2
3
4
@Controller
public class PersonServlet {

}

@AutoWired

@Autowired注解可以自动的为Bean的属性赋值,它的工作原理是:
1.先根据类型去IOC容器中查找Bean
2.如果根据类型查找出来的Bean有多个,再根据属性名作为id去查找
3.如果根据类型查找出来的Bean只有一个,就直接赋值
4.如果根据类型和属性名都查找不出来,就会报错
@Autowired注解可以用在属性上,也可以用在set方法上,也可以用在构造方法上。
修改personDao,添加了save方法

1
2
3
4
5
6
7
8
@Repository    //当只设置value属性时,value可以省略掉  @Repository("personDao")     @Repository
public class PersonDao {

public void save() {
System.out.println("PersonDao..save方法被调用");
System.out.println("------------添加成功-------------");
}
}

修改了PersonService,添加了save方法
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class PersonService {

@Autowired
private PersonDao personDao;

public void save(){
System.out.println("PersonService..save方法被调用");
personDao.save();
}

}

@Autowired
翻译过来叫做:自动装配
@Autowired注解,默认是按照类型自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

在personService运行的时候,就要到IOC容器当中去查找personDao这个类型的对象,而我们的IOC容器中刚好有一个PersonDao这个类型的对象,所以找到了这个类型的对象完成注入操作

修改PersonServlet,添加doGet方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.iweb.controller;

import com.iweb.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

@Controller
public class PersonServlet {

/**
* 属性的自动注入
* 自动装配为属性赋值 Spring一定会去容器中找到这个属性对应的组件
* 该注解遵循的注入的策略是按照类型进行注入
* app.getBean(PersonService.class)
*/
@Autowired
private PersonService personService;

public void doGet(){
System.out.println("PersonServlet..doGet方法被调用");
personService.save();
}
}

注意:如果@Autowired是应用在成员属性上,可以省略set方法

@Resource

该注解是JDK中提供的注解,Spring实现对其的支持,作用也是完成Bean的注入,是按照bean的名称进行注入,通过name属性指定要注入的bean的名称。
修改PersonServlet,添加doGet方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.iweb.controller;

import com.iweb.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

@Controller
public class PersonServlet {

/**
* 属性的自动注入
* 自动装配为属性赋值 Spring一定会去容器中找到这个属性对应的组件
* 该注解遵循的注入的策略是按照类型进行注入
* app.getBean(PersonService.class)
*/
@Resource(name = "personService")
private PersonService personService;

public void doGet(){
System.out.println("PersonServlet..doGet方法被调用");
personService.save();
}
}

面试题:@Autowired vs @Resource的区别

  • @Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解。
  • @Autowired默认是按照类型注入,@Resource是按照名称注入

Spring单元测试环境

在每个测试方法中需要加载IOC容器,从容器中获取需要测试的bean对象

1
2
3
4
// 加载启动
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
// 按照类型获取组件,可以获取到这个类型下的所有实现类子类等等
DataSource ds = app.getBean(DataSource.class);

在现有情况下,这两行代码是必须存在,如果不存在那么测试程序是无法运行的,但是一个类中有多个方法都存在这两行代码,代码重复,问题是能不能直接让程序帮助我完成容器的创建,并且把需要测试的bean对象注入到测试类中呢?

1).完善依赖信息,在pom.xml文件中添加spring-test、junit相关的依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

2).编写单元测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
// 指定用那种驱动进行单元测试环境
@RunWith(SpringJUnit4ClassRunner.class)
// @ContextConfiguration(locations = 使用它来指定Spring要加载的配置文件的位置
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {

@Autowired
private PersonServlet personServlet;
@Test
public void testSave(){
personServlet.doGet();
}
}

AOP操作(不是基础概念)

代理模式

代理模式是23常用设计模式之一
它属于结构型模式,提供了一个代理对象来控制对另一个对象的访问。
代理模式在实际应用中的好处:
1.访问控制:通过代码对象控制对目的对象的访问,增加访问的灵活性
2.职责分离:代理对象可以处理一些额外的职责(日志记录,权限检查),而目的对象专注于核心业务逻辑
3.扩展性:通过代理模式,可以很容器的添加新的功能或修改现有功能,而不需要修改目标对象的代码
代理模式的作用
通过引入一个代理类来控制对目的对象的访问,代理类可以附加一些额外的操作,而不影响目的对象的业务逻辑。在某些情况下,代理对象还可以在客户端和目的对象之间起到中介的作用,避免直接访问目的对象。

代理模式的分类
静态代理:由程序员创建第三方工具生成代理类的源码,再进行编译,其代理类和委托类再编译期间就确定下来的。
代理动态:代理类在程序运行时通过反射等机制动态生成,无需程序员显示的创建代理类。

  • JDK动态代理
  • CGLIB动态代理

JDK动态代理
JDK动态代理是Java提供的一种动态代理机制,它通过反射机制在运行时创建一个代理对象,并将方法调用转发到目标对象上。
JDK动态代理的实现步骤:
1.定义一个接口,例如:PersonService
2.定义一个实现类,例如:PersonServiceImpl
3.创建一个代理类,例如:PersonServiceProxy
4.在代理类中,通过反射机制创建一个代理对象
5.在代理对象中,通过反射机制调用目标对象的方法
6.在代理对象中,添加额外的操作,例如:日志记录、权限检查等
7.返回代理对象
实际案例的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义一个接口
public interface PersonService {
void save();
}
// 定义一个实现类
public class PersonServiceImpl implements PersonService {
@Override
public void save() {
System.out.println("PersonServiceImpl.save");
}
}
// 定义一个代理类
public class PersonServiceProxy implements InvocationHandler {
private PersonService target;
public PersonServiceProxy(PersonService target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("日志记录");
Object result = method.invoke(target, args);
System.out.println("权限检查");
return result;
}
}
// 测试类
public class JdkDynamicProxyTest {
@Test
public void testSave() {
PersonService target = new PersonServiceImpl();
PersonService proxy = (PersonService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new PersonServiceProxy(target)
);
proxy.save();
}
}

如何使用AOP

AOP(面向切面编程)是Spring框架的一个重要特性,它允许我们在不修改业务逻辑代码的情况下,对方法进行增强。AOP主要用于日志记录、性能监控、事务管理等场景。

说人话就是把一个核心功能负责的各个部分分为多个文件分开管理,每个文件负责一个部分,这样就可以把核心功能的代码和其他功能的代码分离开来,互不干扰。

使用AOP的前提——依赖引入
1.在pom.xml文件中添加spring-aop依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- spring核心包 -->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-context</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

<!-- spring AOP -->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-aop</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

2.将目标类添加到IOC容器中
例如:

1
2
3
4
5
6
7
@Service
public class PersonServiceImpl implements PersonService {
@Override
public void save() {
System.out.println("PersonServiceImpl.save");
}
}

以上代码的作用是将PersonServiceImpl类添加到IOC容器中,并且将其命名为personServiceImpl。

每个增删改查的方法前后我们都会有日志记录,我们将这个日志记录的方法叫通知方式,这些通知方法我们可以集中放在一个类中,那么这个类我们可以叫做切面类。
编写通知类(切面类):

1
2
3
4
5
6
7
8
9
@Aspect
@Component
@Aspect
public class LogAspect {
@Before("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void before(){
System.out.println("before");
}
}

以上代码的作用是将LogAspect类添加到IOC容器中,并且将其命名为logAspect。
@Aspect注解的作用是将该类标记为切面类,@Component注解的作用是将该类添加到IOC容器中。
@Before注解的作用是定义一个前置通知,在目标方法执行之前执行,execution表达式用于指定要拦截的方法。

我们学习AOP当中设计到一些核心概念
切点、切面、通知、连接点
1).切点
切点作用就是告诉程序对那些方法记录日志
execution( com.iweb...(..))
上面这一段代码就是切点表达式,意思是匹配com.iweb包下所有类,所有方法,不论参数格式均生效。
2).连接点
连接点就是AOP程序对那些方法生效,这些方法就是连接点。
连接点就是满足切点的各个元素,切点就是所有连接点的集合
3).通知
通知就是AOP具体要执行的逻辑,也就是上面开始计算的方法(@Before、@After)
*4).切面

封装横切逻辑的类(LogAdvice)

通知类型

通知类型一共有五种:

  • @Before:前置通知,在连接前运行之前执行
  • @After:后置通知,在连接点运行之后执行,无论是否有异常都会执行
  • @Around:环绕通知,在连接前后均会执行
  • @AfterReturning:返回后通知,在连接点运行结束后执行,有异常不会执行
  • @AfterThrowing:异常后通知,当连接点运行时发生异常时执行。

案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Aspect
@Component
@Aspect
public class LogAspect {
@Before("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void before(){
System.out.println("before");
}
@After("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void after(){
System.out.println("after");
}
@Around("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before");
joinPoint.proceed();
System.out.println("around after");
}
@AfterReturning("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing("execution(* com.bok.service.impl.PersonServiceImpl.save(..))")
public void afterThrowing(){
System.out.println("afterThrowing");
}
}

上述代码的作用是将LogAspect类添加到IOC容器中,并且将其命名为logAspect。
@Aspect注解的作用是将该类标记为切面类,@Component注解的作用是将该类添加到IOC容器中。
@Before注解的作用是定义一个前置通知,在目标方法执行之前执行,execution表达式用于指定要拦截的方法。
@After注解的作用是定义一个后置通知,在目标方法执行之后执行,无论是否有异常都会执行。
@Around注解的作用是定义一个环绕通知,在目标方法执行前后均会执行。
@AfterReturning注解的作用是定义一个返回后通知,在目标方法执行结束后执行,有异常不会执行。
@AfterThrowing注解的作用是定义一个异常后通知,当目标方法执行时发生异常时执行。

设置SpringAOP的代理方式

1
2
3
4
5
6
7
8
9
10
// 设置SPringAOP的代理方式
// 标识配置类
@Configuration
// 设置注解扫描包
@ComponentScan(basePackageClasses = LogApp.class)
// 注解开启AOP自动代理
@EnableAspectJAutoProxy
public class LogApp {

}

applicationContext.xml开启扫描功能:
1
<context:component-scan base-package="com.bok"></context:component-scan>

完整版的头:(你可以直接搬走啊,不需要写的部分)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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 指定扫描的基础包,把基础包下其子包下的类所有加了注解的类,自动扫描进IOC容器
-->
<context:component-scan base-package="com.iweb"/>


</beans>

声明式事务

声明式事务是指通过配置文件或注解的方式来管理事务,而不是在代码中手动控制事务的提交和回滚。Spring提供了声明式事务的支持,可以通过配置文件或注解来实现。

听到事务那铁定涉及到数据库操作
在Spring中我们可以使用两种声明式事务方式
XML的声明式事务基于注解的声明式事务

XML的声明式事务

Spring框架提供了强大的事务管理支持。它不仅支持编程式事务管理,也支持声明式事务管理。
编程式事务管理:
● 通过在代码中显式地调用事务API来控制事务
声明式事务管理:
● 通过注解或XML配置的方式来管理事务
需求:
采用转账的案例,一个账户给另外一个账户转账,实现的操作是一个账户的金额进行减操作,另外一个账户的金额进行加操作,并且这两个操作都是同一个事务内,要么都成功,要么都失败。
1).maven环境依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!--spring核心依赖-->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-context</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

<!--spring整合jdbc的依赖-->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-jdbc</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

<!--mysql数据库驱动依赖-->
<dependency>
<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>5.1.32</version>

</dependency>

<!--c3p0连接池-->
<dependency>
<groupId>com.mchange</groupId>

<artifactId>c3p0</artifactId>

<version>0.9.5.4</version>

</dependency>

<!--spring事务-->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-tx</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

<!-- aop -->
<dependency>
<groupId>org.springframework</groupId>

<artifactId>spring-aspects</artifactId>

<version>5.1.6.RELEASE</version>

</dependency>

<dependency>
<groupId>aopalliance</groupId>

<artifactId>aopalliance</artifactId>

<version>1.0</version>

</dependency>

<!--单元测试-->
<dependency>
<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.12</version>

</dependency>


2)持久层
1
2
3
4
5
6
7
8
9
10
11
12
//属性定义
package com.iweb.dao;

public interface IAccountDao {

// 金额加操作
public void addMoney(String name,int money);

// 金额减操作
public void subMoney(String name,int money);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//DAO实现类
package com.iweb.dao;

import com.iweb.bean.Account;
import com.iweb.mapper.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Collections;
import java.util.List;

@Repository // 表示是数据访问层 加入到Spring容器中
public class AccountDaoImpl implements IAccountDao {


// JdbcTemplate 是spring框架提供的一个对象,是对原始jdbc API对象的简单封装
@Autowired
private JdbcTemplate jdbcTemplate;


public void addMoney(String name, int money) {
// 金额加操作
String sql="update account set money=money+? where name =?";
jdbcTemplate.update(sql,money,name);
}

public void subMoney(String name, int money) {
// 金额加操作
String sql="update account set money=money-? where name =?";
jdbcTemplate.update(sql,money,name);
}
}

这些是具体案例,在配置完成后可以自行尝试体验学习
3)业务层
1
2
3
4
5
6
7
package com.ibwe.service;

public interface IAccountService {
// 转账操作
public void transfer(String from,String targer,int money);
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.ibwe.service;

import com.iweb.dao.IAccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service // 表示是服务层,加入到Spring容器中
public class AccountServiceImpl implements IAccountService{

@Autowired
private IAccountDao accountDao;


public void transfer(String from, String targer, int money) {
// 减操作
accountDao.subMoney(from,money);
// 加操作
accountDao.addMoney(targer,money);
}
}


4).db.properties
1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
jdbc.user=root
jdbc.password=03101577po

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?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"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- context:component-scan 自动组件扫描
base-package 指定扫描的基础包,把基础包下其子包下的类所有加了注解的类,自动扫描进IOC容器
-->
<context:component-scan base-package="com.iweb"/>

<!-- 加载外部的properties属性文件到IOC容器中-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<!-- 注解开启AOP自动代理 强制使用CGLIB方式进行代理-->
<aop:config proxy-target-class="true"></aop:config>

<!-- 配置模板类 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 配置数据源 -->
<property name="dataSource" ref="dataSource"></property>

</bean>

<!-- 事务管理器
因为事务管理器是对数据库进行操作,所以要加载数据库资源
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>

</bean>

<!-- 事务增强
作用类似与日志管理的通知类的配置
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer*" propagation="REQUIRED" />
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*" rollback-for="java.lang.Exception"/>
</tx:attributes>

</tx:advice>

<!-- 配置切入点和通知使用方法,就是把事务增强使用在具体的方法上,通过切入点表达式
最终进行实现
-->
<aop:config>
<!-- 配置切入点,在那些方法上执行事务管理增强 -->
<aop:pointcut id="txPointcut" expression="execution(* com.iweb.service.*.*(..))"/>
<!-- 配置切入点表达式通知txAdvice把通知用在具体方法上 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>

</beans>

以上部分有点多,但是毕竟就是玩框架就是玩的配置,可以细细对应查看学习

事务标签的属性:
method:*针对那些方法进行事务管理,transfer表示对以transfer开头的方法进行事务管理。 propagation:**事务的传播特性有7种
事务的传播特性共有7种:

1、REQUIRED:如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务【默认】
2、PROPAGATION_REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起;
3、PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
4、PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务;
5、PROPAGATION_MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常;
6、PROPAGATION_NEVER:以非事务方式运行,如果有事务存在,抛出异常;
7、PROPAGATION_NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。

isolation:事务的隔离级别,使用数据库默认的隔离级别
● Oralce采用的是 Read Committed(读已提交)隔离级别
● MySQL数据库采用的,默认隔离级别为Repeatable read (可重复读)
read-only:是否只读,如果是查询则设置为true,可以提高查询性能,增删改则设置为false
rollback-for:出现那些异常时进行事务回滚

基于XML的声明式事务管理比较复杂,使用注解开发AOP是spring框架中推荐的方式,它比XML配置更加简介和直观。

基于注解的声明式事务管理

使用注解得提前打开事务管理器和事务注解驱动

1
2
3
4
5
6
7
8
9
10
<!-- 事务管理器
因为事务管理器是对数据库进行操作,所以要加载数据库资源
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>

</bean>

<!-- 开启事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

2).业务层使用事务注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.iweb.service;

import com.iweb.dao.IAccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service // 表示是服务层,加入到Spring容器中
@Transactional
public class AccountServiceImpl implements IAccountService{

@Autowired
private IAccountDao accountDao;


public void transfer(String from, String targer, int money) {
// 开启事务
// 减操作
accountDao.subMoney(from,money);
//System.out.println(1/0);
// 加操作
accountDao.addMoney(targer,money);
// 提交事务
}
}

我之后会准备一份蓝图文章,专门为快速搭建开发环境和配置复制粘贴。