SpringIOC
IOC容器概念
IOC其实就是一个对象的容器。全称Inversion Of Control 控制反转,核心的作用就是将原来由开发人员来控制的对象管理操作交由Spring来管理。
SpringIOC不仅帮我们管理了对象的创建,还包括给对象增加了生命周期行为、作用域(单例、非单例)、懒加载。 配合Spring的DI, 更能方便的解决对象属性值注入、对象之间的依赖注入问题。
IOC容器初始化方式
ClassPathXmlApplicationContext
加载类路径下的xml配置文件的方式,去初始化IOC容器上下文
// 加载单个xml配置,初始化上下文
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");
// 加载多个xml配置,初始化上下文
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{"applicationContext1.xml", "applicationContext2.xml"});
// 加载当前运行类所在的类路径下所有以application开头的配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("appliction*.xml");
// 加载工程中所有类路径下所有以application开头的配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:appliction*.xml");
// 加载工程中所有类路径下所有以application或spring开头的配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{"classpath*:appliction*.xml", "classpath*:spring*.xml"});
配置文件路径中可以包含通配符(*)和前缀(classpath*:,代表所有类路径,包括源码类路径和单元测试类路径)
AnnotationConfigApplicationContext
加载通过Java注解方式配置的Bean上下文。
// 加载单个注解配置,初始化上下文
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfiguration.class);
// 加载多个注解配置,初始化上下文
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(new Class[]{MyConfiguration1.class, MyConfiguration2.class});
Bean初始化方式
构造方法方式(最常用)
<bean id="xx" class="com.test.StudentDao" />
静态工厂方法
<!--
class:指定的是静态工厂类,而不是将要创建的对象类型
factory-method: 指定的是工厂中的静态方法
-->
<bean id="xx" class="com.test.StudentDaoFactory" factory-method="createDao" />
实例工厂方法
<!--
class:指定的是实例工厂类
-->
<bean id="xxFactory" class="com.test.StudentDaoFactory" />
<!--
factory-bean:指定的是实例工厂对象
factory-method: 指定的是工厂中的实例方法
-->
<bean id="xx" factory-bean="xxFactory" factory-method="createDao" />
Bean的命名
在XML中配置中可以通过标签上的id、name属性值给一个bean命名,以便在其他地方引用。
id属性: bean的唯一名称,只允许出现一个值。且同一个IOC容器中不允许出现两个id值一样的bean。
name属性: 和id类似也是给bean命名。但是name属性的值可以有多个,多个值之间使用英文逗号(,)或者英文分号(;)或者空格符隔开
Bean的作用域
-
prototype
在SpringIOC中prototype scope的意思指的是非单例,就是每次使用该bean的时候都会重新创建一个对象。
-
singleton(默认)
singleton作用域是IOC中默认的作用域,代表单例。每次使用bean的时候,不会重新创建对象,在整个IOC容器中该类型的对象只有一个。
Bean的生命周期
- Singleton Bean的生命周期
- 初始化时机: 在IOC容器初始化时,就会把配置的所有单例bean实例化。
- 销毁时机:在IOC容器销毁时,所有bean的销毁方法会被调用。
- Prototype Bean的生命周期
- 初始化时机: 在实际使用该bean的时候,比如:getBean、获取依赖此bean的其他bean需要使用
- 销毁时机: 在IOC容器销毁时。(但是通过destroy-method指定的声明周期方法不会被调用,也就是说Spring不提供prototypebean完整的生命周期管理)
- 如何指定生命周期的回调方法
- xml中的init-method、destroy-method
- 注解方式@PostConstrutor、@PreDestroy
- 指定默认的声明周期回调方法
- 在xml中,通过在beans标签上添加default-init-method、default-destory-method来指定
- 在注解配置中,没有对应的方法可以设置所有bean默认的生命周期回调
Bean懒加载
lazy-init属性
默认是false
懒加载配置主要是针对单例的bean,因为它默认是在容器初始化时就被实例化了。
如何优雅的停止非Web Spring应用
添加一个shutdown hook。所有派生自ConfigurableApplicationContext接口的实现类都支持此方法
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("xxx.xml");
ctx.registerShutdownHook(); //注册停止回调
Spring DI
概述
DI的全称是Dependency Injection(依赖注入)。IOC是将我们工程中的所有对象交由Spring来管理,DI是此基础,将对象中的属性、依赖的其他对象也管理起来,自动注入到由Spring帮我们管理的对象中。
将要注入的对象和目标对象都必须是由SpringIOC管理的bean.
DI的细节实现
构造参数注入
将一个bean创建过程中构造方法需要的参数,通过Spring DI的方式,自动注入到构造方法中。
Setter注入
先通过一个无参的构造方法创建对象,然后通过属性的setter方法,将属性值注入到对象上。
支持注入的类型
-
普通字面量
- String
- Integer(int)
- Long(long)
- Byte(byte)
- …
-
集和类型
-
List
<bean id="xxx" class="xx.xxx.xxx.AA"></bean> <bean> <property name="hobbies"> <list> <value>简单类型值</value> <bean>内部bean</bean> <ref bean="xxx" /> </list> </property> </bean>
-
Map
<bean id="xxx" class="xx.xxx.xxx.AA"></bean> <bean> <property name="gameTitle"> <map> <entry key="王者荣耀" value="荣耀王者" /> <entry key="王者荣耀" value-ref="xxx" /> </map> </property> </bean>
-
Set
<bean id="xxx" class="xx.xxx.xxx.AA"></bean> <bean> <property name="hobbies"> <!-- set用法和List类似, 里面可以注入普通字面量值、也可以是一个bean引用,或者内部bean、或者是一个set、list、Properties --> <set> <value>简单类型值</value> <bean>内部bean</bean> <ref bean="xxx" /> </set> </property> </bean>
-
java.util.Properties
<!-- props标签是用来注入java.util.Properties类型的属性,用法和map类似,但是属性值是在标签中间写 --> <property name="gameNick"> <props> <prop key="王者荣耀">最擅长1V5</prop> <prop key="吃鸡">一枪爆头</prop> </props> </property>
-
注入空置、空字符串
<property name="gameNick"> <null /> </property> <property name="gameNick" value="" />
-
自动装配
自动装配支持的策略
-
byType
按照类型去IOC容器中找需要的bean,如果找到一个,则自动装配;如果没找到,不注入此属性;如果找到了多个匹配类型的bean,就会报错。
-
byName
按照名称去IOC容器中找需要的bean,如果找到就自动注入;如果没找到,不注入此属性。
-
constructor
工作原理和byType类似,也是按照类型去IOC容器中找对应的bean。不同的是注入的地方不是setter,而是构造方法的参数。
-
no (默认值)
如果没有打开自动注入,默认Spring不会自动装配需要的属性。
XML方式的DI
构造参数注入
-
常规写法
<bean id="xx" class="" /> <bean class="com.test.XXX"> <constructor-arg name="age" value="45" /> <constructor-arg name="x" ref="xx" /> <!-- 注入引用的bean --> <constructor-arg name=""> <!-- 注入内部bean --> <bean></bean> </constructor-arg> <constructor-arg name=""> <!-- 注入list类型构造参数 --> <list> </list> </constructor-arg> .... </bean>
-
c命名空间简写
<bean id="xx" class="" /> <bean class="com.test.XXX" c:age="45" c:x-ref="xx" />
setter方式注入
setter注入能够注入的类型以及写法基本和构造参数注入时的写法一致,只不过将标签换成了
-
常规用法
<bean id="xx" class="" /> <bean class="com.test.XXX"> <property name="age" value="45" /> <property name="x" ref="xx" /> <!-- 注入引用的bean --> <property name=""> <!-- 注入内部bean --> <bean></bean> </property> <property name=""> <!-- 注入list类型构造参数 --> <list> </list> </property> .... </bean>
-
p命名空间简写
<bean id="xx" class="" /> <bean class="com.test.XXX" p:age="45" p:x-ref="xx" />
开启自动装配
<!--
通过给当前的bean添加autowire属性开启自动注入
可选的值:参见自动装配章节
-->
<bean id="xx" class="" autowire="" />
提高自动装配时的权重
<!-- 当其他的bean中需要注入一个Test类型的属性,而满足条件的bean有多个时,会优先注入primary="true"的bean -->
<bean id="xx" class="com.Test" primary="true" />
按类型自动装配时,不参与候选
<!-- 当其他的bean中需要注入一个Test类型的属性,而满足条件的bean有多个时,autowire-candidate="false"的bean会自动退出候选序列 -->
<bean id="xx" class="com.Test" autowire-candidate="false" />
注解方式的DI
-
构造参数注入
在构造方法上添加
@Autowired
注解,构造方法的参数就会自动注入进来 -
setter方法注入
方法有两种:
- 在属性的setter方法上添加
@Autowired
注解 - 在属性上添加
@Autowired
注解
- 在属性的setter方法上添加
提高自动装配时的权重
@Primary
@Component
public class Test {
}
public class Main {
@Primary
@Bean
public void test() {
return new Test();
}
}
SpringAOP
概述
AOP的全称是Aspect Oriented Programming(面向切面编程)
OOP语言提供了类与类之间纵向的关系(继承、接口),而AOP补充了横向的关系(比如在不改变目标类中源代码的情况下给com.john.demo.dao包下所有类中以insert和update开头的方法添加事务管理)
SpringAOP和AspectJ的区别
AspectJ是一个专门主打面向切面编程的框架。 它是使用一种特殊的语言(扩展自Java语言)来编写切面代码,后缀是.aj格式,并且需要使用专门的编译器将其编译成jvm可以运行的class文件。
SpringAOP底层也是使用了AspectJ的方案,但是在上层做了很多封装层面的工作,可以让开发人员直接使用Java代码来编写切面。并且由于使用的是标准的Java语言,所以并不需要在额外安装一个专门的编译器。但是由于开发人员直接接触的是Spring AOP,那么凡是Spring中没有实现的那些AOP功能,我们就用不了了,这种情况下只能跟产品经理撕逼或者去学习原生的AspectJ。
AOP的术语
-
切面(Aspect)
简单来说,切面就是我们要往目标代码中插入进去的代码。
-
连接点(Join Pointer)
理论上所有可能会被切入的地方都可以称之为连接点
-
切入点(Pointcut)
选择某个连接点切入,将切面代码织入进去。这个连接点就叫做切入点。
-
织入(Weaving)
把切面代码糅合到目标代码中的过程就是织入。
-
通知(Advice)
通知决定了切面代码织入到目标代码中后,运行的时机(比如是在目标方法执行前,还是执行后)。
在Spring中使用AOP
基于XML方式使用
-
把aop的schema引入
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" 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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
-
创建一个切面类,并且以bean的方式配置到IOC容器中
package com.lanou3g.spring; public class MyAspect { public void wakeup() { System.out.println("[前置通知]我刚学习SpringAOP睡着了,刚才谁打了我一下?"); } public void goToBed() { System.out.println("[后置通知]SpringAOP太难了,一不小心又睡着了"); } public void afterRetuing(Object message) { System.out.println("[后置返回通知]方法执行已经return了,方法返回值是:" + message); } public void afterThrowing(Throwable ex) { System.out.println("[后置异常通知]方法执行出现异常,异常原因:" + ex.getMessage()); } /** * 环绕通知 * 可以接受一个ProceedingJoinPoint参数 * 通过此参数可以获取到被切入方法的所有信息 * 还可以通过此参数来决定是否调用目标方法 */ public void aroundAdvice(ProceedingJoinPoint joinPoint) { // 连接点参数可以获取到被切入方法的所有信息 // 这里演示了如何获取被切入方法的名称 String targetMethodName = joinPoint.getSignature().getName(); System.out.println("[环绕通知]被切入的方法名:" + targetMethodName); // System.out.println("[环绕通知]即将开始新的一天, 早起的鸟儿有虫吃!"); try { joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("[环绕通知]这真是充实的一天, 早睡早起,方能养生!"); } }
<bean id="myAspect" class="com.lanou3g.spring.MyAspect" />
3.使用aop:config标签配置aop(将切面、切入点、通知结合到一起)
- 定义切入点表达式
- aop:aspect
- 引用外部定义的切面bean
- 配置通知,引用切入点表达式
<aop:config>
<!-- 切入点表示匹配com.lanou3g.spring包下的所有类中所有以oneDay开头的方法,方法的参数、返回值不限 -->
<aop:pointcut id="myPointcut" expression="execution(* com.lanou3g.spring..*.oneDay*(..))" />
<aop:aspect ref="myAspect">
<!-- 无论是否出现异常,只要被切入的方法开始运行,都会触发此通知 -->
<aop:before method="wakeup" pointcut-ref="beforeOneDay" />
<!-- 无论是否出现异常,只要被切入的方法运行结束,都会触发此通知 -->
<aop:after method="goToBed" pointcut-ref="beforeOneDay" />
<!--
可以最大限度的对被切入方法附加功能,在方法执行前、后都可以通知(无论是否出现异常)
,还可以获取到被切入方法的所有信息,包括是否调用被切入的方法
-->
<aop:around method="aroundAdvice" pointcut-ref="beforeOneDay" />
<!-- 被切入的方法正常返回值以后,会触发此通知 -->
<aop:after-returning method="afterRetuing" pointcut-ref="beforeOneDay" returning="message" />
<!-- 被切入的方法抛出异常以后,会触发此通知,并且不会触发after-returning -->
<aop:after-throwing method="afterThrowing" pointcut-ref="beforeOneDay" throwing="ex" />
</aop:aspect>
</aop:config>
基于注解方式使用
-
开启AOP注解支持
方式一:注解的方式
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
方式二:xml中开启
<aop:aspectj-autoproxy/>
- 定义切面类
/** * 该切面用来插入起床的逻辑 */ @Aspect @Component //@Aspect注解没有将bean交给ioc容器管理的功能 public class MyAspect { @Before("com.lanou3g.spring.aop.MyPointcut.allOneDayMehtod()") public void wakeup() { System.out.println("[前置通知]我刚学习SpringAOP睡着了,刚才谁打了我一下?"); } @After("com.lanou3g.spring.aop.MyPointcut.allOneDayMehtod()") public void goToBed() { System.out.println("[后置通知]SpringAOP太难了,一不小心又睡着了"); } @AfterReturning(value = "com.lanou3g.spring.aop.MyPointcut.allOneDayMehtod()", returning = "message") public void afterRetuing(Object message) { System.out.println("[后置返回通知]方法执行已经return了,方法返回值是:" + message); } @AfterThrowing(value = "com.lanou3g.spring.aop.MyPointcut.allOneDayMehtod()", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("[后置异常通知]方法执行出现异常,异常原因:" + ex.getMessage()); } /** * 环绕通知 * 可以接受一个ProceedingJoinPoint参数 * 通过此参数可以获取到被切入方法的所有信息 * 还可以通过此参数来决定是否调用目标方法 */ // @Around("com.lanou3g.spring.aop.MyPointcut.allOneDayMehtod()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) { // 连接点参数可以获取到被切入方法的所有信息 // 这里演示了如何获取被切入方法的名称 String targetMethodName = joinPoint.getSignature().getName(); System.out.println("[环绕通知]被切入的方法名:" + targetMethodName); // System.out.println("[环绕通知]即将开始新的一天, 早起的鸟儿有虫吃!"); Object ret = null; try { ret = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("[环绕通知]这真是充实的一天, 早睡早起,方能养生!"); return ret; } }
注意:@Aspect注解没有将bean交给ioc容器管理的功能,我们需要额外添加一个@Component注解
-
定义切入点
官方建议我们将所有的切入点统一定义到一个地方管理,在配置通知时通过引入的方式来使用。方便后期维护(一处修改,处处生效)
@Component
public class MyPointcut {
// 通过@Pointcut注解定义一个切入点
@Pointcut("execution(* oneDay(..))")
public void allOneDayMehtod() {}
}
- 在切面类中添加要切入的代码
参见定义切面部分
- 在切入的代码方法上添加通知的注解
参见定义切面部分
动态代理
动态代理特点
字节码随用随创建,随用随加载
它与静态代理的区别也在于此,。因为静态代理是字节码一上来就创建好,并完成加载。
装饰者模式就是静态代理的一种体现。
动态代理常用的有两种方式
- 基于接口的动态代理
提供者:JDK 官方的 Proxy 类。
要求:被代理类最少实现一个接口。
- 基于子类的动态代理
提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
要求:被代理类不能用 final 修饰的类(最终类)。
使用JDK官方的Proxy类创建代理对象
对生产厂家要求的接口
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
一个生产者
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
模拟一个消费者
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法。
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
使用CGLib的Enhancer类创建代理对象
消费者
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
*
* Callback:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
解决的问题
例:事务处理中每项操作都需要有开启事务,执行业务方法,提交事务,回滚事务,释放资源这几个操作,我们可以把重复代码提取出来
public class BeanFactory {
/**
* 创建账户业务层实现类的代理对象
* @return
*/
public static IAccountService getAccountService() {
//1.定义被代理对象
final IAccountService accountService = new AccountServiceImpl();
//2.创建代理对象
IAccountService proxyAccountService = (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(),new InvocationHandler() {
/**
* 执行被代理对象的任何方法,都会经过该方法。
* 此处添加事务控制
*/
@Override
public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
Object rtValue = null;
try {
//开启事务
TransactionManager.beginTransaction();
//执行业务层方法
rtValue = method.invoke(accountService, args);
//提交事务
TransactionManager.commit();
}catch(Exception e) {
//回滚事务
TransactionManager.rollback();
e.printStackTrace();
}finally {
//释放资源
TransactionManager.release();
}
return rtValue;
}
});
return proxyAccountService;
}
}
改造完成后,业务层用于控制事务的重复代码就都可以删掉了
AOP实现
AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类:
- 静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段通过AOP框架指令生成 AOP 代理类,因此也称为编译时增强;还有一种静态代理是编写代码实现不用工具;这种方式一般是代理模式会使用。
- 动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。