1、Spring
1.1 简介
- Spring:春天---->给软件行业带来了春天!
- 2002,首次推出了Spring框架的雏形:interface21框架!
- Spring框架即以interface21框架为基础,经过重新设计,并不断丰富内涵,于2004年3月24日,发布了1.0正式版。
- Rod Johnson,Spring Framework创始人,著名作者。很难想象其学历,真的让好多人大吃一惊,他是悉尼大学的博士,然而他的专业不是计算机,而是音乐学。
- spring理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架。
- SSH:Struct2+Spring+Hibernate!
- SSM:SpringMVC+Spring+Mybatis!
官网:https://spring.io/projects/spring-framework#overview
官方下载地址:https://repo.spring.io/release/org/springframework/spring/
Github:https://github.com/spring-projects/spring-framework
Maven仓库:导入webmvc包会自动导入相关依赖;jdbc用于和Mybatis整合。
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
1.2 优点
-
Spring是一个开源的免费的框架(容器),它包含并且管理应用对象的生命周期 !
-
Spring是一个轻量级的、非入侵式的框架!
-
依赖注入:DI——Dependency Injection,反转控制(IOC)最经典的实现。、面向切面编程(AOP)!
-
支持事务的处理,对框架整合的支持!
-
非侵入式:基于 Spring 开发的应用中的对象可以不依赖于 Spring 的 API
-
组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。
总结一句话:Spring就是一个轻量级的控制反转(IOC)和面向切面编程的框架!
1.3 组成
1.4 拓展
在Spring的官网有这个介绍:现代化的java开发!说白了就是基于Spring的开发!
- Spring Boot
- 一个快速开发的脚手架。
- 基于Spring Boot可以快速的开发单个微服务。
- 约定大于配置!
- Spring Cloud
- SpringCloud是基于SpringBoot实现的。
因为现在大多数公司都在使用SpringBoot进行快速开发,学习SpringBoot的前提,需要完全掌握Spring以及SpringMVC!承上启下的作用。
弊端:发展了太久之后,违背了原来的理念!配置十分繁琐,人称:“配置地狱”。
2、 IOC理论推导
1.UserDao接口
2.UserDaoImpl实现类
3.UserService业务接口
4.UserServiceImpl业务实现类
在我们之前的业务中,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求去修改源代码!如果程序代码量十分大,修改一次的成本代价十分昂贵!
我们使用一个Set接口实现,已经发生了革命性的变化!
private UserDao userDao;
//利用set进行动态实现值的注入!
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
- 之前,程序是主动创建对象!控制权在程序员手上!
- 使用了set注入后,程序不再具有主动性,而是变成了被动的接收对象!
这种思想,从本质上解决了问题,我们程序员不用再去管理对象的创建了。系统的耦合性大大降低,可以更加专注在业务的实现上。这是IOC的原型!
IOC本质
**控制反转IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IOC的一种方法,**也有人认为DI是IoC的另一种说法。没有IoC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。
采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IoC容器,其实现方式是依赖注入(Dependency Injection,DI)。
3、 Hello Spring
beans.xml官网配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
bean对象添加:
<bean id="mysqlImpl" class="com.kuang.dao.UserDaoMysqlImpl"></bean>
<bean id="oracleImpl" class="com.kuang.dao.UserDaoOracleImpl"></bean>
<bean id="UserServiceImpl" class="com.kuang.service.UserServiceImpl">
<!--
ref:引用Spring容器中已经创建好的对象
value:具体的值,基本数据类型
-->
<property name="userDao" ref="mysqlImpl"></property>
</bean>
Test方法:
//解析beans.xml文件,生成管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//getBean:参数即为spring配置文件中bean的id
Hello hello = (Hello) context.getBean("hello");
System.out.println(hello.toString());
思考问题?
-
Hello对象是谁创建的?
hello对象是由Spring创建的。
-
Hello对象的属性是怎么设置的?
hello对象的属性是由Spring容器设置的。
这个过程就叫做控制反转:
控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用Spring后,对象是由Spring来创建的。
反转:程序本身不创建对象,而变成被动的接收对象。
依赖注入:就是利用set方法来进行注入。
IoC是一种编程思想,由主动的编程编程被动的接收。
可以通过new ClassPathXmlApplicationContext去浏览一下底层源码。
OK,到了现在,我们彻底不用在程序中去改动了,要实现不同的操作,只需要在xml配置文件中进行修改,所谓的IoC,一句话搞定:对象由Spring来创建,管理,装配!
IDEA快捷创建beans.xml文件,自动导入spring配置信息:
4、 IoC创建对象的方式
-
使用无参构造创建对象,通过属性名,默认方式!
-
构造器注入。
1.下标赋值。
<!--第一种,下标赋值!--> <bean id="user" class="com.kuang.pojo.User"> <constructor-arg index="0" value="憨批" /> </bean>
2.类型赋值。
<!--第二种,通过类型创建,不建议使用,重复类型难以分辨--> <bean id="user" class="com.kuang.pojo.User"> <constructor-arg type="java.lang.String" value="大憨批" /> </bean>
3.参数名赋值。
<!--第三种,直接通过参数名来设置--> <bean id="user" class="com.kuang.pojo.User"> <constructor-arg name="name" value="臭憨批" /> </bean>
注意 : 在配置文件加载的时候,容器中管理的对象就已经初始化了!
5、 Spring配置
5.1 别名
<!--别名,如果添加了别名,我们也可以使用别名获取到-->
<alias name="user" alias="userNew"></alias>
5.2 Bean的配置
<!--
id:bean的唯一标识符,相当于我们学的对象名;
class:bean对象所对应的全限定名:包名+类名;
name:也是别名,可以同时取多个别名,逗号分割
-->
<bean id="userT" class="com.kuang.pojo.UserT" name="user2,u2">
</bean>
5.3 import
这个import,一般用于团队开发使用,他可以将多个配置文件,导入合并为一个。
假设,现在项目中有多个人开发,这三个人负责不同的类开发,不同的类需要注册在不同的bean中,我们可以利用import将所有人的beans.xml合并为一个总的!
- 张三
- 李四
- 王五
- applicationContext.xml
<import resource="beans.xml"/>
<import resource="beans2.xml"/>
<import resource="beans3.xml"/>
使用的时候,直接使用总的配置就可以了。
6、 依赖注入
6.1 构造器注入
之前已经介绍过。
6.2 Set方式注入【重点】
- 依赖注入:Set注入!
- 依赖:bean对象的创建依赖于容器!
- 注入:bean对象中的所有属性,由容器来注入!
【环境搭建】
-
复杂类型
public class Address { private String address; public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
-
真实测试对象
public class Student { private String name; private Address address; private String[] books; private List<String> hobbies; private Map<String,String> card; private Set<String> games; private String wife; private Properties info; }
-
beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="student" class="com.kuang.pojo.Student"> <!--第一种,普通值注入--> <property name="name" value="憨批"/> </bean> </beans>
-
测试类
public class MyTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Student student = (Student) context.getBean("student"); System.out.println(student.getName()); } }
-
完善注入:
<?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="address" class="com.kuang.pojo.Address"/>
<bean id="student" class="com.kuang.pojo.Student">
<!--第一种,普通值注入,value-->
<property name="name" value="憨批"/>
<!--第二种,Bean注入,ref-->
<property name="address" ref="address"/>
<!--数组注入-->
<property name="books">
<array>
<value>红楼梦</value>
<value>西游记</value>
<value>水浒传</value>
<value>三国演义</value>
</array>
</property>
<!--List注入-->
<property name="hobbies">
<list>
<value>听歌</value>
<value>敲代码</value>
<value>看电影</value>
</list>
</property>
<!--Map-->
<property name="card">
<map>
<entry key="身份证" value="1555555555"/>
<entry key="银行卡" value="5555555555"/>
</map>
</property>
<!--Set-->
<property name="games">
<set>
<value>lol</value>
<value>wow</value>
</set>
</property>
<!--null-->
<property name="wife">
<null/>
</property>
<!--Properties-->
<property name="info">
<props>
<prop key="driver">com.mysql.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/news</prop>
<prop key="root">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
</beans>
6.3 工厂方法注入
-
调用静态工厂方法创建 Bean
- 调用静态工厂方法创建 Bean是将对象创建的过程封装到静态方法中. 当客户端需要对象时, 只需要简单地调用静态方法, 而不同关心创建对象的细节.
- 要声明通过静态方法创建的 Bean,
1. 在 Bean 的 class 属性里指定拥有该工厂的方法的类,
2. 在 factory-method 属性里指定工厂方法的名称
3. 使用<constrctor-arg>
元素为该方法传递方法参数. -
调用实例工厂方法创建 Bean
- 实例工厂方法: 将对象的创建过程封装到另外一个对象实例的方法里. 当客户端需要请求对象时, 只需要简单的调用该实例方法而不需要关心对象的创建细节.
- 要声明通过实例工厂方法创建的 Bean
1. 在 bean 的 factory-bean 属性里指定拥有该工厂方法的 Bean
2. 在 factory-method 属性里指定该工厂方法的名称
3. 使用 construtor-arg 元素为工厂方法传递方法参数 -
实现 FactoryBean 接口在 Spring IOC 容器中配置 Bean
- Spring 中有两种类型的 Bean, 一种是普通Bean, 另一种是工厂Bean, 即FactoryBean.
- 工厂 Bean 跟普通Bean不同, 其返回的对象不是指定类的一个实例, 其返回的是该工厂 Bean 的 getObject 方法所返回的对象
6.4 拓展方式注入
我们可以使用c和p命令空间进行注入:
使用:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
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">
<!--p命名空间注入,可以直接注入属性的值:property-->
<bean id="user" class="com.kuang.pojo.User" p:name="憨批" p:age="18"/>
<!--c命名空间注入,通过构造器注入:construct-args-->
<bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批"/>
</beans>
测试:
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("userBeans.xml");
User user = context.getBean("user2", User.class);
System.out.println(user);
}
注意点:p和c命名空间不能直接使用,需要导入xml约束!
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
6.5 配置细节
- 字面值
• 字面值:可用字符串表示的值,可以通过<value>
元素标签或 value 属性进行注入。
• 基本数据类型及其封装类、String 等类型都可以采取字面值注入的方式
• 若字面值中包含特殊字符,可以使用 <![CDATA[]]> 把字面值包裹起来。 - 引用其他bean
• 组成应用程序的 Bean 经常需要相互协作以完成应用程序的功能. 要使 Bean 能够相互访问, 就必须在 Bean 配置文件中指定对 Bean 的引用
• 在 Bean 的配置文件中, 可以通过<ref>
元素或 ref 属性为 Bean 的属性或构造器参数指定对 Bean 的引用
• 也可以在属性或构造器里包含 Bean 的声明, 这样的 Bean 称为内部 Bean - 内部bean
• 当 Bean 实例仅仅给一个特定的属性使用时, 可以将其声明为内部 Bean. 内部 Bean 声明直接包含在<property>
或<constructor-arg>
元素里, 不需要设置任何 id 或 name 属性
• 内部 Bean 不能使用在任何其他地方 - 注入参数详解:null 值和级联属性
• 可以使用专用的<null/>
元素标签为 Bean 的字符串或其它对象类型的属性注入 null 值
• 和 Struts、Hiberante 等框架一样,Spring 支持级联属性的配置 - 集合属性
• 在 Spring中可以通过一组内置的 xml 标签(例如:<list>, <set> 或 <map>
) 来配置集合属性.
-
List 和 数组
• 配置 java.util.List 类型的属性, 需要指定<list>
标签, 在标签里包含一些元素. 这些标签可以通过<value>
指定简单的常量值, 通过<ref>
指定对其他 Bean 的引用. 通过<bean>
指定内置 Bean 定义. 通过<null/>
指定空元素. 甚至可以内嵌其他集合.
• 数组的定义和 List 一样, 都使用<list>
-
Set 集合
• 配置 java.util.Set 需要使用<set>
标签, 定义元素的方法与 List 一样. -
Map 集合
• Java.util.Map 通过<map>
标签定义,<map>
标签里可以使用多个<entry>
作为子标签. 每个条目包含一个键和一个值.
• 必须在<key>
标签里定义键
• 因为键和值的类型没有限制, 所以可以自由地为它们指定 , , 或 元素.
• 可以将 Map 的键和值作为 的属性定义: 简单常量使用 key 和 value 来定义; Bean 引用通过 key-ref 和 value-ref 属性定义 -
Properties
• 使用<props>
定义 java.util.Properties, 该标签使用多个 作为子标签. 每个 标签必须定义 key 属性.
6.6 bean的作用域
-
代理模式(Spring默认机制):get到的都是同一个对象!
<bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批" scope="singleton"/>
-
原型模式:每次从容器中get的时候,都会产生一个新的对象!
<bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批" scope="prototype"/>
-
其余的request、session、application、这些个只能在web开发中使用。
6.7 之间的关系:继承;依赖
• Spring 允许继承 bean 的配置, 被继承的 bean 称为父 bean. 继承这个父 Bean 的 Bean 称为子 Bean
• 子 Bean 从父 Bean 中继承配置, 包括 Bean 的属性配置
• 子 Bean 也可以覆盖从父 Bean 继承过来的配置
• 父 Bean 可以作为配置模板, 也可以作为 Bean 实例. 若只想把父 Bean 作为模板, 可以设置 <bean>
的abstract 属性为 true, 这样 Spring 将不会实例化这个 Bean
• 并不是 <bean>
元素里的所有属性都会被继承. 比如: autowire, abstract 等.
• 也可以忽略父 Bean 的 class 属性, 让子 Bean 指定自己的类, 而共享相同的属性配置. 但此时 abstract 必须设为 true
• Spring 允许用户通过 depends-on 属性设定 Bean 前置依赖的Bean,前置依赖的 Bean 会在本 Bean 实例化之前创建好
• 如果前置依赖于多个 Bean,则可以通过逗号,空格或的方式配置 Bean 的名称
7、 Bean的自动装配
- 自动装配是Spring满足bean依赖的一种方式!
- Spring会在上下文中自动寻找,并自动给bean装配属性!
在Spring中有三种装配的方式:
- 在xml中显式配置;
- 在java中显式配置;
- 隐式的自动装配bean
7.1 测试
环境搭建:一个人有两个宠物!
7.2 ByName自动装配
<!--
byName:会自动在容器上下文中查找和自己对象set方法后面的值对应的beanid!
-->
<bean id="people" class="com.kuang.pojo.People" autowire="byName">
<property name="name" value="憨批"/>
</bean>
7.3 ByType自动装配
<!--
byType:会自动在容器上下文中查找,和自己对象属性类型相同的bean!必须保证类型全局唯一。
-->
<bean id="people" class="com.kuang.pojo.People" autowire="byType">
<property name="name" value="憨批"/>
</bean>
小结:
- byName的时候,需要保证所有bean的id唯一,并且这个bean需要和自动注入的属性的set方法的值一致!
- byType的时候,需要保证所有bean的class唯一,并且这个bean需要和自动注入的属性的类型一致!
7.4 使用注解实现自动装配
jdk1.5支持注解,Spring2.5开始支持注解。
要使用注解须知:
-
导入约束:context约束。
-
配置注解的支持:context:annot-config/
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config/> </beans>
@Autowired
直接在属性上使用即可!也可以在set方式上使用!
使用Autowired我们可以不用编写Set方法了,前提是你这个自动装配的属性在IoC(Spring)容器中存在,且符合名字byName!
科普:
@Nullable 字段标记了这个注解,说明这个字段可以为null
public People(@Nullable String name){
this.name = name;
}
public @interface Autowired {
boolean required() default true;
}
测试代码:
public class People {
//如果显式定义了Autowired的required属性为false,说明这个对象可以为null,否则不允许为空
@Autowired(required = false)
private Dog dog;
@Autowired
private Cat cat;
private String name;
}
如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解@Autowired完成的时候,我们可以使用@Qualifier(value=“xxx”)去配置@Autowired的使用,指定一个唯一的bean对象注入!
public class People {
@Autowired
@Qualifier(value="dog11")
private Dog dog;
@Autowired
@Qualifier(value="cat11")
private Cat cat;
private String name;
}
@Resource注解
public class People {
@Resource(name = "cat2")
private Cat cat;
}
小结:
@Resource和@Autowired的区别:
-
都是用来自动装配的,都可以放在属性字段上;
-
@Autowired通过byType的方式实现,而且必须要求这个对象存在!【常用】
-
@Resource默认通过byName的方式实现,如果找不到名字,则通过byType实现!如果两个都找不到的情况下,就报错!
-
执行顺序不同:@Autowired通过byType的方式实现,@Resource默认通过byName的方式实现。
8、 使用注解开发
在spring4之后,要使用注解开发,必须要保证aop的包导入了。
使用注解需要导入context约束,增加注解的支持!
<!--指定要扫描的包,这个包下的注解会生效-->
<context:component-scan base-package="com.kuang.pojo"/>
-
bean
-
属性如何注入
//等价于<bean id="user" class="com.kuang.pojo.User"/> //@Component 组件 @Component public class User { //相当于<property name="name" value="小憨批"/> public String name; @Value("小憨批") public void setName(String name){ this.name = name; } }
-
衍生的注解
@Component有几个衍生注解,我们在web开发中,会按照mvc三层架构分层!
- dao【@Repository】
- service【@Service】
- controller【@Controller】
这四个注解功能都是一样的,都是代表将某个类注册到Spring中,装配Bean!
-
自动装配
-@Autowired:自动装配通过类型,名字 如果Autowired不能唯一自动装配上属性,则需要通过@Qualifier(value="xxx") -@Nullable:字段标记了这个注解,说明这个字段可以为null -@Resource:自动装配通过名字,类型
-
作用域
@Scope("singleton") public class User { //相当于<property name="name" value="小憨批"/> public String name; @Value("小憨批") public void setName(String name){ this.name = name; } }
-
小结
xml与注解:
- xml更加万能,适用于任何场合!维护简单方便。
- 注解,不是自己的类使用不了,维护相对复杂!
xml与注解最佳实践:
- xml用来管理bean;
- 注解只负责完成属性的注入;
- 我们在使用的过程中,只需要注意一个问题:必须让注解生效,就需要开启注解的支持。
<!--指定要扫描的包,这个包下的注解会生效--> <context:component-scan base-package="com.kuang"/> <context:annotation-config/>
- 对于扫描到的组件, Spring 有默认的命名策略: 使用非限定类名, 第一个字母小写. 也可以在注解中通过 value 属性值标识组件的名称
- base-package 属性指定一个需要扫描的基类包,Spring 容器将会扫描这个基类包里及其子包中的所有类.
- 当需要扫描多个包时, 可以使用逗号分隔.
- 如果仅希望扫描特定的类而非基包下的所有类,可使用 resource-pattern 属性过滤特定的类,示例:
<context:include-filter>
子节点表示要包含的目标类
<context:exclude-filter>
子节点表示要排除在外的目标类
<context:component-scan>
下可以拥有若干个 <context:include-filter>
和 <context:exclude-filter>
子节点
<context:include-filter>
和 <context:exclude-filter>
子节点支持多种类型的过滤表达式:
9、 使用java的方式配置Spring
我们现在要完全不适用Spring的xml配置了,全权交给java来做!
javaConfig是Spring的一个子项目,在Spring4之后,它成为了一个核心功能。
实体类:
@Component
public class User {
private String name;
public String getName() {
return name;
}
@Value("小笨蛋")
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
import com.kuang.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
//这个也会被Spring容器托管,注册到容器中,因为本来就是一个@Component
//@Configuration代表这是一个配置类,就和我们之前看的beans.xml
@Configuration
@ComponentScan("com.kuang.pojo")
@Import(KuangConfig2.class )
public class KuangConfig {
//注册一个bean,就相当于我们之前写的一个bean标签
//这个方法的名字,就相当于bean标签中的id属性
//这个方法的返回值,就相当于bean标签中的class属性
@Bean
public User getUser(){
return new User();//就是返回要注入到bean的对象
}
}
测试类:
public class MyTest {
public static void main(String[] args) {
//如果完全使用了配置类方式去做,我们就只能通过AnnotationConfig上下文来获取容器,通过配置类的class对象加载!
ApplicationContext context = new AnnotationConfigApplicationContext(KuangConfig.class);
User getUser = (User) context.getBean("getUser");
System.out.println(getUser.getName());
}
}
这种纯java的配置方式,在SpringBoot中随处可见!
10、 代理模式
为什么要学习代理模式?因为这就是SpringAOP的底层!【SpringAOP 和 SpringMVC 面试必问】
代理模式的分类:
- 静态代理
- 动态代理
10.1 静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作。
- 客户:访问代理对象的人!
代码步骤:
-
接口
//租房 public interface Rent { public void rent(); }
-
真实角色
//房东 public class Host implements Rent { public void rent(){ System.out.println("房东要出租房子!"); } }
-
代理角色
public class Proxy implements Rent { private Host host; public Proxy() { } public Proxy(Host host) { this.host = host; } public void rent(){ seeHouse(); host.rent(); hetong(); fee(); } //看房 public void seeHouse(){ System.out.println("中介带你看房"); } //签合同 public void hetong(){ System.out.println("签合同"); } //收费 public void fee(){ System.out.println("收取中介费用"); } }
-
客户端访问
public class Client { public static void main(String[] args) { //房东要租房子 Host host = new Host(); //代理,中介帮房东租房子,但是呢?代理角色一般会有一些附属操作! Proxy proxy = new Proxy(host); proxy.rent(); } }
代理模式的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共也就交给代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
缺点:
- 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低
10.2 加深理解
聊聊AOP
10.3 动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的。
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口——JDK动态代理
- 基于类:cglib
- java字节码实现:javasisit
需要了解两个类:Proxy:代理;InvocationHandler:调用处理程序
- 基于jdk实现的动态代理
import com.proxy.daili.service.IModelMath;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 动态代理模式类
* 第一种代理模式:Jdk动态代理
* 注意:实现InvocationHandler这个接口
*
* 基于接口的
*/
public class JdkDynamicProxy implements InvocationHandler {
//定义需要代理的接口
protected IModelMath iModelMath;
//将需要代理的接口作为参数传入到动态代理设计模式类中
public JdkDynamicProxy(IModelMath iModelMath){
this.iModelMath = iModelMath;
}
/**
* 生成代理对象
* 使用java.lang.reflect.Proxy这个类调用newProxyInstance方法
* 返回 动态代理类对象
*/
public IModelMath iModelMathmethod(){
IModelMath iModelMathProxy = (IModelMath) Proxy.newProxyInstance(iModelMath.getClass().getClassLoader(),
iModelMath.getClass().getInterfaces(),
this);
return iModelMathProxy;
}
/**
* 开始做代理的操作
* Object proxy 代理对象的引用
* Method method 当前执行的方法
* Object[] args 当前执行方法的参数
* 返回 与被代理对象返回的值相同
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("你调用的方法为:"+method.getName());
System.out.println("你调用的方法参数有:"+ Arrays.toString(args));
Object invoke = method.invoke(iModelMath, args);
System.out.println("方法的返回数据:"+invoke);
return invoke;
}
}
import com.proxy.daili.service.IModelMath;
import com.proxy.daili.JdkDynamicProxy;
import com.proxy.daili.service.ModelMath;
import org.junit.Test;
public class TestJDKDynamicProxy {
/**
* 使用jdk方式的动态代理
* 测试
*/
@Test
public void testJdkDynamicProxy(){
//需要被代理的动态对象
IModelMath imm = new ModelMath();
//代理对象
IModelMath math = new JdkDynamicProxy(imm).iModelMathmethod();
//通过代理对象做操作
int addition = math.addition(10, 2);
int subtraction = math.subtraction(20, 19);
System.out.println("实际方法的数据为:"+addition);
System.out.println("实际方法的数据为:"+subtraction);
}
}
- 基于gcLib实现的动态代理
import com.proxy.daili.service.IModelMath;
import com.proxy.daili.service.ModelMath;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* cglib动态代理设计类
* 前提必须要先导入 cglib 包
* 基于 实现类的
*/
public class CglibDynamicProxy implements MethodInterceptor {
//定义被代理的实现类(注意这 是实现类,不是接口)
private ModelMath modelMath;
//将被代理的对象作为参数 传入到 cglib动态代理设计类中
public CglibDynamicProxy(ModelMath modelMath){
this.modelMath = modelMath;
}
//生成代理对象
public ModelMath getProxyModelMath(){
//new 一个Enhancer对象
Enhancer enhancer = new Enhancer();
//指定他的父类(注意这 是实现类,不是接口)
enhancer.setSuperclass(ModelMath.class);
//指定真正做事情的回调方法
enhancer.setCallback(this);
//生成代理类对象
ModelMath o = (ModelMath) enhancer.create();
//返回
return o;
}
/**
* 执行被代理的任何方法,都会经过这个方法
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("通过gclib 动态代理调用的方法名为:"+method.getName());
System.out.println("通过gclib 动态代理调用的方法的参数包含:"+ Arrays.toString(objects));
Object invoke = method.invoke(modelMath, objects);
System.out.println("通过gclib 动态代理调用的方法返回的数据:"+ invoke);
return invoke;
}
}
import com.proxy.daili.CglibDynamicProxy;
import com.proxy.daili.service.ModelMath;
import org.junit.Test;
public class TestCgLibDynamicProxy {
/**
* 使用gclib方式的动态代理
* 测试
*/
@Test
public void testCglibDynamicProxy(){
ModelMath modelMath = new ModelMath();
ModelMath proxyModelMath = new CglibDynamicProxy(modelMath).getProxyModelMath();
int subtraction = proxyModelMath.subtraction(1, 44);
int addition = proxyModelMath.addition(10, -1);
System.out.println("执行减法得到的正式数据为:"+subtraction);
System.out.println("执行加法得到的正式数据为:"+addition);
}
}
动态代理的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务交给代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
- 一个动态代理类代理类代理的是一个接口,一般就是对应的一类业务
- 一个动态代理类可以代理多个类,只要是实现了同一个接口即可!
11、 AOP
11.1 什么是AOP
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生泛型,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的频率。
AOP 术语
实现机制 : AOP通过代理模式实现,Spring的AOP通过JDK动态代理实现。
- 切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象,可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为“在哪干和干什么集合”;
- 通知(Advice): 切面必须要完成的工作,在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;AOP中通知有多种类型。在AOP中通知表示为“干什么”;
- 目标(Target): 被通知的对象,需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为“被通知对象”;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为“对谁干”;
- 代理(Proxy): AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。
- 织入(Weaving):织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行。
- 连接点(Joinpoint):程序执行的某个特定位置,表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为“在哪里干”; 连接点由两个信息确定: 方法表示的程序执行点和相对点表示的方位. 例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
- 切入点(Pointcut):选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为“在哪里干的集合”;
在AOP中,通过切入点选择目标对象的连接点,然后在目标对象的相应连接点处织入通知,而切入点和通知就是切面(横切关注点),而在目标对象连接点处应用切面的实现方式是通过AOP代理对象。
每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
11.2 AOP在Spring中的作用
提供声明式事务;允许用户自定义切面
- 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点,如日志、安全、缓存、事务等等……
- 切面(ASPECT):横切关注点被模块化的特殊对象,即是一个类。
- 通知(Advice):切面必须要完成的工作,即是类中的一个方法。
- 目标(Target):被通知对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。
- 切入点(PointCut):切面通知执行的“地点”的定义。
- 连接点(jointPoint):与切入点匹配的执行点。
通知类型:
SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:
- 前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
- 返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
- 抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
- 后通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
- 环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
即AOP在不改变原有代码的情况下,去增加新的功能。
11.3 使用注解实现AOP
1. 【重点】使用AOP织入,需要导入一个依赖包。
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
- 将 aop Schema 添加到
<beans>
根元素中. - 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素
<aop:aspectj-autoproxy>
- 当 Spring IOC 容器侦测到 Bean 配置文件中的
<aop:aspectj-autoproxy>
元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.
2. 用 AspectJ 注解声明切面
- 要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例. 当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理.
- 在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.
- 通知是标注有某种注解的简单的 Java 方法.
- AspectJ 支持 5 种类型的通知注解:
- @Before: 前置通知, 在方法执行之前执行
- @After: 后置通知, 在方法执行之后执行
- @AfterRunning: 返回通知, 在方法返回结果之后执行
- @AfterThrowing: 异常通知, 在方法抛出异常之后
- @Around: 环绕通知, 围绕着方法执行
- 可以在通知方法中声明一个类型为 JoinPoint 的参数. 然后就能访问链接细节. 如方法名称和参数值.
① 前置通知
前置通知:在方法执行之前执行的通知
前置通知使用 @Before 注解, 并将切入点表达式的值作为注解值.
@Before("pointcutRef()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method "+methodName+" begins"+args);
}
② 后置通知
后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止.
一个切面可以包括一个或者多个通知.
@After("pointcutRef()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method "+methodName+" ends");
}
③ 返回通知
- 无论连接点是正常返回还是抛出异常, 后置通知都会执行. 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.
- 在返回通知中, 只要将 returning 属性添加到 @AfterReturning 注解中, 就可以访问连接点的返回值.
- 该属性的值即为用来传入返回值的参数名称.
- 必须在通知方法的签名中添加一个同名参数. 在运行时, Spring AOP 会通过这个参数传递返回值.
原始的切点表达式需要出现在 pointcut 属性中
@AfterReturning(pointcut="pointcutRef()",returning="a")
public void afterReturning(Object a){
System.out.println(a+"aa");
}
④ 异常通知
- 只在连接点抛出异常时才执行异常通知
- 将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常. Throwable 是所有错误和异常类的超类. 所以在异常通知方法可以捕获到任何错误和异常.
- 如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
@AfterThrowing(pointcut="pointcutRef()",throwing="e")
public void afterThrowing(Exception e){
System.out.println(e);
}
⑤ 环绕通知
- 环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点. 甚至可以控制是否执行连接点.
- 对于环绕通知来说, 连接点的参数类型必须是 ProceedingJoinPoint . 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点.
- 在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
- 注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
@Around(value = "pointcutRef()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
Object proceed = null;
try {
System.out.println("我是前置通知");
proceed = proceedingJoinPoint.proceed();
System.out.println("我是返回通知 + " + proceed);
} catch (Throwable e) {
System.out.println("我是异常通知"+ e);
e.printStackTrace();
}
System.out.println("我是后置通知");
return proceed;
}
3. 指定切面的优先级
- 在同一个连接点上应用不止一个切面时, 除非明确指定, 否则它们的优先级是不确定的.
- 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定.
- 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高.
- 若使用 @Order 注解, 序号出现在注解中
4. 利用方法签名编写 AspectJ 切入点表达式
- 最典型的切入点表达式时根据方法的签名来匹配各种方法:
* execution * com.atguigu.spring.ArithmeticCalculator.*(..):
* 匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值. 第二个 * 代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
* execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.
* execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中返回 double 类型数值的方法
* execution public double ArithmeticCalculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
* execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
- 在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来.
重用切入点定义
- 在编写 AspectJ 切面时, 可以直接在通知注解中书写切入点表达式. 但同一个切点表达式可能会在多个通知中重复出现.
- 在 AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法. 切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的.
- 切入点方法的访问控制符同时也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下, 它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中, 还必须包含包名.
- 其他通知可以通过方法名称引入该切入点.
@Pointcut("execution( * com.xzj.cyg.spring.CounterInpl.*(int,int))")
public void pointcutRef(){}
11.4 基于XML配置文件的 AOP
- 除了使用 AspectJ 注解声明切面, Spring 也支持在 Bean 配置文件中声明切面. 这种声明是通过 aop schema 中的 XML 元素完成的.
- 正常情况下, 基于注解的声明要优先于基于 XML 的声明. 通过 AspectJ 注解, 切面可以与 AspectJ 兼容, 而基于 XML 的配置则是 Spring 专有的. 由于 AspectJ 得到越来越多的 AOP 框架支持, 所以以注解风格编写的切面将会有更多重用的机会.
① 基于 XML ---- 声明切面
- 当使用 XML 声明切面时, 需要在
<beans>
根元素中导入 aop Schema - 在 Bean 配置文件中, 所有的 Spring AOP 配置都必须定义在
<aop:config>
元素内部. 对于每个切面而言, 都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端 Bean 实例. - 切面 Bean 必须有一个标示符, 供
<aop:aspect>
元素引用
② 基于 XML ---- 声明切入点
- 切入点使用
<aop:pointcut>
元素声明 - 切入点必须定义在
<aop:aspect>
元素下, 或者直接定义在<aop:config>
元素下.- 定义在
<aop:aspect>
元素下: 只对当前切面有效 - 定义在
<aop:config>
元素下: 对所有切面都有效
- 定义在
- 基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点.
③ 基于 XML ---- 声明通知
- 在 aop Schema 中, 每种通知类型都对应一个特定的 XML 元素.
- 通知元素需要使用
<pointcut-ref>
来引用切入点, 或用<pointcut>
直接嵌入切入点表达式. method 属性指定切面类中通知方法的名称.
<bean id="logging" class="com.xzj.cyg.spring2.LoggingAspect"></bean>
<aop:config>
<aop:pointcut expression="execution(* *.*(..))" id="mypointcut"/>
<aop:aspect id="loggingAspect" ref="logging" >
<aop:after method="afterMethod" pointcut-ref="mypointcut"/>
<aop:before method="beforeMethod" pointcut-ref="mypointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="mypointcut" returning="a"/>
</aop:aspect>
</aop:config>