Spring 之IOC 容器和AOP 面向切面编程
spring 是 Java EE 开发中的核心框架。spring 的核心分为两部分 IOC 和 AOP。spring 是Java EE 全栈级框架。通过spring 我们可以很好的实现程序的解耦和Java 类之间依赖关系的管理与维护,==声明式事务管理==。通过spring 框架可以很方便的整合其他的开源框架。
![image-20200820001516743 image-20200820001516743](https://img-blog.csdnimg.cn/img_convert/ed5c1e1470374bb8182e0c0aff7a5d4b.png)
IOC
程序的耦合
谈到 IOC 我们不得不先提及一下程序的耦合。我们在开发中一直追求的是模块的高内聚低耦合。高内聚指的是模块内联系的紧密程度。耦合则指的是模块之间的联系。我们模块越是高内聚低耦合,开发出来的模块代码独立性越高,这也正是我们所追求的。
耦合的分类:
内容耦合:在一个模块内直接修改或操作另一个模块的数据。相当于把两个模块集成到了一起。耦合度高,不利于维护。
公共耦合:两个或多个模块共同引用操作一个全局的公共模块或数据项,相当于把重复模块独立出来。
外部耦合:一组模块访问统一全局的简单变量而不是同一数据结构。这种方式通过传递参数传递全局数据信息
控制耦合:一个模块向另一个模块传递控制信号,来控制另一模块的行为。
标记耦合:若一个模块 A 通过接口向两个模块 B 和 C 传递一个公共参数,那么称模块 B 和 C 之间存在一个标记耦合。
数据耦合:模块之间通过参数传递数据。
非直接耦合:两个模块之间没有联系,它们之间联系完全通过主模块通知与调用来完成。
从上到下,模块的耦合性递减。
IOC
为了解决程序的耦合性,我们在进行JDBC 学习时,通过利用 配置文件 的方式配置数据库驱动,连接信息。让我们加载数据库信息不再硬编码在代码中,实现动态从配置文件中获取,运行时编译检查,实现了程序的解耦。
Spring 采用了工厂解耦的方式实现了把我们开发中的三层的对象统一进行配置管理,维护依赖关系。我们不在需要手动去 new 对象,而是统一交给spring 创建和管理对象,我们在使用时只需要去像 spring 索要就行,这就是IOC 的概念和思想。
IOC 本质上是一个 Java bean 的工厂,容器。通过 IOC 统一实现对象的创建与管理。因此 IOC 的核心功能便是 控制反转(创建对象)依赖注入(索取对象)。注意控制反转包含依赖查找和依赖注入。
![image-20200820003135841 image-20200820003135841](https://img-blog.csdnimg.cn/img_convert/eb67f721617615581c72b5a5cd25d26f.png)
IOC 实现的两种方式
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- 在使用配置配置 spring 时我们需要引入 spring 约束 -->
<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-3.2.xsd">
<!-- 控制反转 把创建对象的权利交给spring IOC -->
<!-- 通过 bean 标签来让spring 创建对象加入IOC 容器 id 类在ioc中的标识 class 类的全路径 scope ioc创建对象的模式 singleton 单例 prototype 多例 spring 默认创建的是单例对象 注意采用这种方式注入 javabean类必须存在默认的无参构造-->
<bean id="helloSpring1" class="com.spring.demo.HelloSpring" scope="singleton">
</bean>
<!-- 通过静态工程实例化Bean factory-mathod 静态工厂中的方法
public class StaticFactory {
public static HelloSpring createHelloSpring(){
return new HelloSpring();
} }
注意这种方式创建Bean时 工厂类要要为静态类或者工厂方法为静态方法,原因是静态属性(方法)在类加载还没创建对象时就实例
化到了JVM内存中
-->
<bean id="helloSpring2" class="com.spring.demo.HelloSpring" factory-method="createHelloSpring">
</bean>
<!--通过实例工厂创建JavaBean
public class InstanceFactory {
public HelloSpring createHelloSpring(){
return new HelloSpring();
} }
实例工厂创建Bean 的方式是先创建一个工厂bean,然后通过工厂方法创建另一个Bean
相当于前两种方式的结合。
-->
<bean id="HelloSpringfactory" class="com.spring.demo.InstanceFactory"></bean>
<bean id="hellospring3" factory-bean="HelloSpringfactory" factory-method="createHelloSpring"></bean>
<!--
我们实例化Bean 到spring IOC 的方式有三种。其实都大同小异。不难理解。需要特别提及的一个点是一个类可以在Ioc中实例化多个
Bean。还有一个关于声明周期的,我使用较少,所以也就稍微一列:
<bean id="" class="" init-method="" destroy-method="" />
bean对象的作用范围与生命周期:
单例对象:一个应用只有一个实例对象,作用范围是整个引用。
当IOC容器加载,对象就被创建,加载到IOC容器中。
只要容器存在,对象就一直存活。
当容器销毁时,对象销毁。
多例对象:每次访问引用对象都会创建一个实例Bean
当引用使用对象时,对象创建。
对象只要被使用,就一直存活
对象长时间不被使用,被GC回收
-->
<!-- 注入Bean 的方式 依赖注入: 从IOC 中获取java 对象来创建新的Bean 对象加入Ioc
注入Bean 的方式分为三种:构造函数注入,set方法,p命名空间注入(p 命名空间注入基本不使用,所以我也不再提及)
-->
<!--
基本的Person 类我就不写了,大家可以自行脑补
通过构造函数注入,创建 bean 对象时调用有参构造函数。主要利用 <constructor-arg> 这个标签
constroctor标签用于向构造函数传参数: name代表的是参数名, value用于设定基本类型的参数和String的参数
ref 用于引入其他的Bean类型参数
-->
<bean id="Person" class="com.spring.demo.Person" scope="singleton">
<constructor-arg name="name" value="stack"></constructor-arg>
<constructor-arg name="age" value="15"></constructor-arg>
<constructor-age name="birthDay" ref="now"></constructor-age>
</bean>
<bean id="now" class="java.util.Date"></bean>
<!-- set 方法注入
set 方法注入要求实体类必须符合java Bean 的规范就是存在无参构造,属性私有,提供set/get 方法
set 方法注入 主要利用了 property 标签,其它和构造函数注入一样。
通过set 方式注入属性我们更常使用
-->
<bean id="Person" class="com.spring.demo.Person" scope="singleton">
<property name="name" value="stack"></constructor-arg>
<property name="age" value="15"></constructor-arg>
<property name="birthDay" ref="now"></constructor-age>
</bean>
<bean id="now" class="java.util.Date"></bean>
<!-- 扩展给集合注入值,因为这种方式不经常使用,所以简单了解下,其实给集合注入和set 方法注入本质上一样 -->
<bean id="Hello" class="com.spring.demo.Hello">
<!-- set 集合值不可以重复 -->
<property name="myset">
<set>
<value>aaa</value>
<value>bbb</value>
</set>
</property>
<property name="mylist">
<list>
<value>aaa</value>
<value>bbb</value>
</list>
</property>
<property name="mymap">
<props>
<propy key="test">hello</propy>
<propy key="test2">hello2</propy>
</props>
</property>
<!-- 注入properties 类,properties 配置文件类本质也是一个键值类型的map 集合 -->
<property name="myProps">
<map>
<entry key="testA" value="aaa"></entry>
<entry key="testB">
<value>bbb</value>
</entry>
</map>
</property>
</bean>
</beans>
注解
关于注解的使用我就在这里简单列举一下,因为平常经常使用,所以就不设计具体代码了。spring 使用注解要开启注解扫描,引入spring 支持注解的相关jar 包
具体的做法就是在spring 的配置文件中配置标签 <context:component-scan base-package="com.stack"/>,配置注解要扫描到的包,当然如果在springBoot这一步也就进行省略,因为 springBoot 已经帮我们配置。当然通过 spring,我们也可以进行全注解配置,在下面我也会介绍到全配置的注解。
用于声明 Bean 的注解:<bean id="" class="">
@Component(value="***")类上, 配置任意一个类型的Bean的,常用于一些工具类上注册为一个Ioc 对象,当一个组件使用,相当于
<bean id="" class=""> value等价于id,class就是当前类。
@Service @Controller @Repository 和@Component 一样,不过就是用于三层模型的不同位置,便于区分。
上面的注解 value 可以省略,默认为类名首字母小写
用于注入相关的注解:相当于 set 方法注入,当使用下面注解时,set方法可以省略
@AutoWrite: 按照类型注入属性,
@value("**"): 用于注入基本数据类型和String
@Resource:按照bean 的id ,id 不指定时默认为参数名,所以要求要求参数名是类名的驼峰规则转换
@Qualifier:在自动按照类型注入的基础之上,再按照 Bean 的 id 注入。它在给字段注入时不能独立使用,必须和@Autowire 一起使用;
但是给方法参数注入时,可以独立使用 。不常用
改变作用范围的:
@scope("singleton") propotype (request session globalsession:web 相关 不经常使用)
声明周期相关(了解):<bean id="" class="" init-method="" destroy-method="" />
通过方法返回Bean 的注解
@Bean(name="***"): name一般不省略,如果省略的话依然是类名首字母小写,注意此注解只能作用在方法上,用于方法返回bean,和刚才
xml配置中的工厂返回bean 一样。
spring 5新注解
@Configuration: 声明为配置类 value 默认为类的字节码名
@Import:导入其他配置类的,
@PropertySource:加载配置文件的: 实例 @Value("${jdbc.driver}")
@ComponentScan:用于扫描注解包,取代<context:component-scan base-package="com.stack"/>
实例:
@Configuration
@ComponentScan(basePackages = "com.itheima.spring")
@Import({ JdbcConfig.class})
获取IOC容器加载IOC 中的bean
说了这么多配置,我们到底如和获取容器进行配置呢。下面就简单贴下代码
xml获取IOC 容器。加载对象
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
// 验证默认 value 属性 是否是指代 id (注解中的 value 属性相当于 bean 标签中的id 注解默认值为类名的首字母小写)
UserService userService= (UserService) applicationContext.getBean("userService");
userService.print();
注解 加载配置类获取IOC 容器。加载对象
ApplicationContext applicationContext=new AnnotationConfigApplicationContext(SpringConfiguration.class);
// 验证默认 value 属性 是否是指代 id (注解中的 value 属性相当于 bean 标签中的id 注解默认值为类名的首字母小写)
UserService userService= (UserService) applicationContext.getBean("userService");
userService.print();
![image-20200821221325328](https://img-blog.csdnimg.cn/img_convert/46bd335053cfef9c2ec60f6579555b78.png)
![image-20200821221630002 image-20200821221630002](https://img-blog.csdnimg.cn/img_convert/96c251667a8e6f2c68fece00abea92c0.png)
AOP
代理设计模式
说道AOP 就不得不提及一下代理设计模式,因为 AOP 的实现时基于代理设计模式。什么是代理设计模式呢,代理设计模式指的是我们在访问操作一个 Java类时,不直接去访问操作这个Java 类,而是通过一个类去代理实现这个类,代理类可以实现已有方法上的增强。代理模式一般分为静态代理和动态代理。静态代理是通过一个具体实现的代理类去进行代理。这种方式具有较大的局限性,比如一个代理类只能去代理一类类,不同的类要想被代理,需要去手动编写一个新代理类,及其不方便,还会造成代码的冗余。动态代理则是利用反射机制,在运行时注入被代理的类,可以动态的代理不同类型的对象,Java中动态代理分为两类一类是基于接口的动态代理(proxy,类必须实现一个接口),一种是基于子类的动态代理(cglib,类不能是最终类)。通过动态代理可以提取出代码中的重复部分,减少冗余,可以动态实现已有方法的增强。
AOP
AOP 面向切面编程,什么是面向切面编程呢,就是把原有的方法看成一个切面,我们可以在这个切面的前后进行一些操作,动态实现已有代码的增强,而不是把原方法当做一个执行的单元体。spring 提供了AOP 面向切面编程,在使用AOP 时我们不需要去关注代理反射的代码怎样去写,采用哪种代理方式(proxy 或 cglib)。spring 把这些重复复杂的操作屏蔽掉,我们只需要进行简单的配置,就可以实现动态代理,对重复代码进行抽取,实现方法增强,注意spring 只支持方法级别的增强。spring aop 的典型事例就是 spring 为我们提供的声明式事务管理。
概念
连接点:指的是将要被拦截到的点,spring,指的是进行拦截到的方法,spring只支 持方法级别的拦截点。说白点,连接点就是你将要执行增强(添加功能)的方法。
切入点与切入点表达式:指的是对将要拦截到的点或方法的统一的定义/表达式
增强/通知:增强或通知指的就是对拦截到的拦截点将要进行的操作。增强和通知的类型分为:前置通知,后置通知,异常通知,最终通知,环绕通知
切面:切面指的是切入点拦截到的点与增强的结合。
织入:对增强类,生成代理对象的过程。spring 采用动态代理织入,通过基于接口 proxy 或者基于子类 cglib 生成代理对象。
代理:对一个类建立一个代理类,通过使用代理类对被代理类执行操作。代理分为静态代理(实现类)和动态代理(反射机制)。动态代理又分为基于接口 proxy 和基于子类 cglib 进行动态代理,spring 会自动选择代理放式。
切入点表达式
![image-20200821223930899](https://img-blog.csdnimg.cn/img_convert/521ce28f9d6ae23c1d90c8d3847eb645.png)
![image-20200821224012615](https://img-blog.csdnimg.cn/img_convert/7c5c685c90e8b06c91edd91fe4d5d19c.png)
AOP 的两种实现方式
在这里被代理的类就没写,自行脑补,实现代理功能的类,底下的注解类便是。
配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--ioc 容器配置-->
<bean id="logger" class="com.stack.aop.Logger">
</bean>
<bean id="userService" class="com.stack.service.impl.UserServiceImpl">
</bean>
<aop:config>
<!-- aop 配置 切入点 就是指代 哪些将要被增强的方法 连接点 是指 运行时被拦截到点,一般就是具体被增强的方法 spring 只支持方法级别拦截-->
<aop:pointcut id="log" expression="execution(* com.stack.service.impl.*.*(..))"/>
<!-- 引入包含增强方法的类 切面 增强 与 切入点 结合-->
<aop:aspect ref="logger">
<aop:before method="before" pointcut-ref="log"/>
<aop:after-returning method="after" pointcut-ref="log" returning="result"/>
</aop:aspect>
</aop:config>
</beans>
注解
使用注解首先要开启扫描注解配置
<!--ioc 容器开启注解扫描-->
<context:component-scan base-package="com.stack" />
<!--- aop 注解配置支持--> 或者使用 @EnableAspectJAutoProxy 注解,@EnableAspectJAutoProxy应该出现在配置类上
<aop:aspectj-autoproxy />
@Configuration
@ComponentScan(basePackages="com.itheima")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
@Aspect // 声明为切面类
@Component //加入 ioc 容器
public class Logger {
@Pointcut("execution(* com.stack.service.impl.*.*(..))") //相当于配置切入点表达式 它与在方法上的 @bean 返回 ioc 中的组件不同 方法级别的注解
public void pointcut(){
}
/**
* 前置通知
* 注意需要导入的包
* @param joinPoint
*/
@Before("pointcut()") //需要引入切入点表达式 知道去哪个切面操作
public void before(JoinPoint joinPoint){
System.out.println(joinPoint.getTarget()+"的"+joinPoint.getSignature()+"方法开始执行,参数为:"+joinPoint.getSignature());
}
/**
* 后置通知
* @param joinPoint
* @param result
*/
@AfterReturning(pointcut = "pointcut()",returning = "result") //需要指定参数名和返回值
public void after(JoinPoint joinPoint,Object result){
System.out.println(joinPoint.getTarget()+"的"+joinPoint.getSignature()+"方法开始执行,参数为:"+joinPoint.getSignature()+"返回值:"+result);
}
}
上面我只是写了几个增强类型,其它类型我前边也有说明,具体的标签和注解可以自行百度。spring AOP 的典型应用就是声明式事务支持,后面会拿出一门文章来写。