Spring 框架
Spring概述
Spring模块示意图:
Core Container:核心容器(IOC)
AOP+Aspects:面向切面编程模块
DataAccess/Integration:数据访问模块
Web:Spring开发Web应用模块
IOC
控制反转——控制并反转资源的获取方式,由程序员自己创建资源转变为由容器创建和设置资源。
DI: Dependency Injection 依赖注入
spring这个容器中,替你管理着一系列的类,前提是你需要将这些类交给spring容器进行管理,然后在你需要的时候,不是自己去定义,而是直接向spring容器索取,当spring容器知道你的需求之后,就会去它所管理的组件中进行查找,然后直接给你所需要的组件.
bean属性
id:bean的唯一标识
class:组件的全类名
bean的生命周期
通过scope属性设置bean的生命周期:
prototype 多实例:
1.在容器启动时不会创建实例,从容器中获取bean时才创建实例
2.每一次获取实例的结果都不一样
singleton 单实例:
1.在容器启动完成之前就创建好bean的实例
2.每一次获取实例的结果都是之前创建的实例
request:
Web环境下,对象与request生命周期一致
session:
Web环境下,对象与session生命周期一致
生命周期属性:
初始化方法:init-method
销毁方法:destroy-method
<!--
单实例bean的生命周期:
(容器开启时创建)构造器->初始化方法->销毁方法(容器关闭时销毁 ConfigurableApplicationContext中才有close方法)
多实例bean的生命周期:
获取bean(构造器->初始化方法)->容器关闭不会调用bean的销毁方法
-->
<bean id="book1" class="com.first.bean.Book" destroy-method="myDestory" init-method="myInit">
<property name="bookName" value="xixixi"></property>
</bean>
创建bean
1.创建一个空的bean:
<bean id="person1" class="com.first.bean.Person">
</bean>
<!--
工厂模式:有一个专门帮我们创建对象的类帮我们创建对象
静态工厂:工厂本身不用创建对象,通过静态方法调用
实例工厂:工厂本身需要创建对象
-->
2.利用静态工厂创建bean:
<!--静态工厂-->
<bean id="airPlane1" class="com.first.factory.AirPlaneStaticFactory" factory-method="getAirPlane">
</bean>
3.利用实例工厂创建bean:
<!--实例工厂-->
<bean id="airPlaneInstanceFactory" class="com.first.factory.AirPlaneInstanceFactory"></bean>
<bean id="airPlane2" class="com.first.bean.Airplane" factory-bean="airPlaneInstanceFactory" factory-method="getAirPlane">
</bean>
给bean注入值
1.set注入:
基本类型使用value注入,引用类型使用ref注入,默认注入null,若要给有默认值的属性注入null,则使用null标签.
<bean id="person1" class="com.first.bean.Person">
<!--使用property标签为Person对象的属性赋值-->
<property name="age" value="18"></property>
<!--ref 引用外面的bean-->
<property name="car" ref="car1"></property>
<!--给有默认值的属性注入空值-->
<property name="name">
<null></null>
</property>
</bean>
2.使用有参构造器注入:
在bean中定义一个有参构造函数.
public Person(String name, Integer age, String gender, String email) {
this.name = name;
this.age = age;
this.gender = gender;
this.email = email;
System.out.println("调用有参构造器...");
}
配置文件:
<bean id="person3" class="com.first.bean.Person">
<!--调用有参构造器给bean赋值-->
<constructor-arg name="age" value="20"></constructor-arg>
<constructor-arg name="email" value="hihi@163.com"></constructor-arg>
<constructor-arg name="gender" value="male"></constructor-arg>
<constructor-arg name="name" value="hihi"></constructor-arg>
<!--省略name属性时,必须严格按照参数顺序构造-->
</bean>
3.名称空间注入
引入p名称空间:在beans标签中加入
xmlns:p="http://www.springframework.org/schema/p"
<!--名称空间:在xml中名称空间是用来防止标签重复的-->
<bean id="person4" class="com.first.bean.Person" p:age="100" p:email="100@163.com" p:name="qwer" p:gender="female"></bean>
4.SPEL注入:
<!--SpEL(spring 表达式语言)测试-->
<!--在SpEL中使用字面量-->
<bean id="person4" class="com.first.bean.Person">
<!--字面量:#{}--><!--使用运算符-->
<property name="salary" value="#{12345.67*12}"></property>
<!--引用其他bean的某个属性值-->
<property name="name" value="#{book1.bookName}"></property>
<!--引用其他bean-->
<property name="car" value="#{car}"></property>
<!--调用静态方法
#{T(全类名).静态方法名(1,2)}
-->
<property name="email" value="#{T(java.util.UUID).randomUUID().toString().substring(0,5)}"></property>
<!--调用其他非静态方法-->
<property name="gender" value="#{book1.getBookName()}"></property>
</bean>
5.注入list:
<bean id="person3" class="com.first.bean.Person">
<property name="name" value="曹嘤嘤"></property>
<property name="books">
<list>
<!--list标签体中添加每一个元素-->
<bean id="book0001" class="com.first.bean.Book" p:author="吴承恩" p:bookName="西游记"></bean>
<!--引用外部一个元素-->
<ref bean="book0002"></ref>
</list>
</property>
</bean>
6.注入map:
<bean id="person4" class="com.first.bean.Person">
<property name="name" value="emmm"></property>
<property name="maps">
<map>
<entry key="key01" value="张三"></entry>
<entry key="key02" value="haha"></entry>
<!--引用一个外部创建好的bean-->
<entry key="key03" value-ref="car1"></entry>
</map>
</property>
</bean>
使用utils名称空间注入:
引入utils名称空间
<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:utils="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
<!--还有这两行-->
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.0.xsd">
定义一个map:
<utils:map id="myMap">
<entry key="key1" value="掌声"></entry>
<entry key="key2" value="29"></entry>
<entry key="key3" value-ref="car1"></entry>
</utils:map>
定义bean:
<bean id="person6" class="com.first.bean.Person">
<property name="maps" ref="myMap">
</property>
</bean>
7.注入properties:
<bean id="person5" class="com.first.bean.Person">
<property name="properties">
<props>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
8.注入级联属性:
<bean id="person7" class="com.first.bean.Person">
<property name="car" ref="car1"></property>
<property name="car.price" value="500000"></property>
</bean>
9.加载外部配置文件:
<context:property-placeholder location="classpath:jdbc.properties"/>
使用注解创建bean(官方推荐)
使用注解将组件快速的加入到容器中需要:
添加注解
spring有四个注解:
@Controller 控制器 给控制器(servlet)层的组件加
@Service 业务逻辑
@Repository 给数据库层(持久化,Dao层)的组件添加这个注解
@Component 给不属于以上几层的组件添加这个注解
注解可以随便加,spring底层不会验证组件是否如注解所说.
1.组件的id默认就是组件的类名首字母小写,设置组件id在注解后加括号,括号中设置id.
@Repository(“id”)
2.组件的作用域,默认是单例的
@Scope(value=“prototype”) //设置为多实例
引入context名称空间自动扫描
context:component-scan:自动组件扫描
base-package:指定扫描的基础包,spring会将基础包及下面所有包中加了注解的类自动扫描进ioc容器
<context:component-scan base-package="com.first"></context:component-scan>
<!--使用context:exclude-filter指定扫描包时不包含的类-->
<context:component-scan base-package="com.first">
<!--扫描的时候可以排除一些不必要的组件
type="annotation":指定排除规则,按照注解进行排除.标注了指定注解的类不要
expression="":注解的全类名
type="assignable":指定排除某个具体的类
expression="":类的全类名
上述两种最常用
type="aspectj":aspectj表达式
type="custom":自定义一个TypeFilter,自己写代码决定哪些使用
type="regex":正则表达式
-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--使用context:include-filter指定扫描包时要包含的类-->
<context:component-scan base-package="com.first" use-default-filters="false">
<!--只扫描进入哪些组件,默认是全部扫描进来,
此时要禁用默认的过滤规则:use-default-filters="false"
-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
使用@Autowired注解实现根据类型自动装配
<!--@Autowired原理:
1.先按照类型去容器中找到对应组件 bookService = ioc.getBean(BookService);
1)找到一个:直接赋值
2)没找到:抛异常
3)找到多个:
1)按照变量名作为id继续匹配-->
<!--如果资源类型的bean不止一个,默认根据@Autowired注解标记的成员变量名作为id查找bean,进行装配 -->
<!--如果根据成员变量名作为id还是找不到bean,可以使用@Qualifer注解明确指定目标bean的id -->
<!--@Autowired注解的required属性指定某个属性允许不被设置 required = false -->
//自动装配,自动的为这个属性赋值
//@Qualifier:指定一个名字作为id,让spring别使用变量名作为id.
@Qualifier("bookService")
//@Autowired(required = false) 找不到就装配null
@Autowired
private BookService bookServiceExt2;
<!--Autowired和resource的区别:
resource:扩展性更强,如果切换成另一个容器框架也能支持
autowired:spring自己的注解-->
在方法参数上添加@Qualifer注解:
/**
* 方法上有autowired,这个方法的每一个参数都会注入值,且会在创建bean时自动运行
*/
@Autowired
public void hahaha(BookDao bookDao, @Qualifier("bookService")BookService bookService){
System.out.println("Spring运行了这个方法..." + bookDao + "," + bookService);
}
一定要导入aop包,支持加注解模式
使用注解加入到容器中的组件,和使用配置加入到容器中的组件行为是默认一样的
Spring的单元测试
@RunWith(SpringJUnit4ClassRunner.class)//spring测试包
@ContextConfiguration(“classpath:applicationContext.xml”) //配置文件的地址
AOP
面向切面编程:指在程序运行期间,将某段代码动态的切入到指定方法的指定位置进行运行
术语解释
JoinPoint(连接点):目标对象中,所有可以增强的方法,就是spring允许你是通知(Advice)的地方,那可就真多了,基本每个方法的前、后(两者都有也行),或抛出异常是时都可以是连接点,spring只支持方法连接点。
Pointcut(切入点):目标对象中,已经被增强的方法。调用这几个方法之前、之后或者抛出异常时干点什么,那么就用切入点来定义这几个方法。
Advice(通知/增强) :增强方法的代码、想要的功能。
Target(目标对象):被代理对象,被通知的对象,被增强的类对象。
Weaving(织入):将通知应用到连接点形成切入点的过程
Proxy(代理):将通知织入到目标对象之后形成的代理对象
aspect(切面):切入点+通知————通知(Advice)说明了干什么的内容(即方法体代码)和什么时候干(什么时候通过方法名中的before,after,around等就能知道),二切入点说明了在哪干(指定到底是哪个方法),切点表达式等定义。
使用步骤
1.导包
spring-aop + spring-aspects + springsource.org.aopalliance + springsource.org.aspectj.weaver
2.写配置
1,将目标类和切面类(封装了通知方法(在目标方法执行前后执行的方法))加入到ioc容器中
2.告诉Spring哪个是切面类
3.告诉Spring,切面类里面的每一个方法都是何时何地运行
4.开启基于注解的AOP功能(或使用配置文件)
3.测试
重要的用配置,不重要的用注解
基于注解的AOP
先引入AOP名称空间,再加上下面代码:
<aop:aspectj-autoproxy proxy-target-class="true"/>
开启使用注解。
将切面类加载入ioc容器
使用@Aspect注解可以将切面类加载入ioc容器
使用@Order(Integer)注解可以改变切面加载顺序,数值越小越优先
AOP的5个注解
5个通知注解:
前置通知:@Before():在目标方法之前运行
后置通知:@After():在目标方法结束之后运行
返回通知:@AfterReturning():在目标方法正常返回之后
异常通知:@AfterThrowing():在目标方法抛出异常之后运行
环绕通知:@Around():围绕方法执行
try{
//@Before
method.invoke();
//@AfterReturning
}catch(e){
//@AfterThrowing
}finally{
//@After
}
书写切点表达式
上述5个注解在()内设置value值可指定切点位置
/**
* 方法切入点表达式:execution(访问权限符 返回值类型 方法签名)
* 支持通配符:
* *:
* 1.匹配一个或多个字符:execution(public int com.third.impl.MyMathCalculator.*(int,int))
* 2.匹配任意一个参数:
* 3.只能匹配一层路径
* 4.权限位置不能用*号表示,想要表示任意权限,不写就行
* ..:
* 1.匹配任意多个参数和任意类型参数
* 2.匹配任意多层路径
* 支持&&,||,!运算
*/
/**
* 抽取可重用的切入点表达式:
* 1.随便声明一个没有实现的返回void的方法
* 2.给方法上标注@Pointcut注解
*/
@Pointcut("execution(public int com.third.impl.MyMathCalculator.*(int,int))")
public void hahaMyPoint(){};
得到方法返回值
使用@AfterReturning注解中的returning属性可以获取方法调用后的返回值.
eg:
/**
* 使用AfterReturning注解中的returing来接收返回值,returing = 接收变量名称
* @param joinPoint 连接点
* @param result 返回值
*/
@AfterReturning(value = "hahaMyPoint()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("[Logs][" + name +"]方法正常执行完成,结果为[" + result + "]");
}
得到方法执行时的异常
/**
* 在通知方法运行时,拿到目标方法的详细信息
* 1.给通知方法的参数列表上添加JoinPoint参数:
* 2.使用throwing告诉spring接收异常的变量
* 3.Exception尽量往大了写,写小了便只能接收那一类的异常
*/
@AfterThrowing(value = "hahaMyPoint()",throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("[Logs][" + name + "]方法出现异常,异常信息是:" + e.getMessage());
}
环绕通知
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
eg:
@Around("hahaMyPoint()")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
Object proceed = null;
try {
System.out.println("方法前置通知");
//就是利用反射调用目标方法
proceed = pjp.proceed(args);
System.out.println("方法正常执行通知");
} catch (Exception e){
System.out.println("方法异常执行通知");
e.printStackTrace();
throw new RuntimeException(e);//不加这行外界感受不到
} finally {
System.out.println("方法结束执行通知");
}
return proceed;
}
按方法执行顺序输出结果
在使用除环绕通知外的其他四个通知时,方法的输出如下:
/**
* 通知方法的执行顺序:
* 正常执行:@Before(前置通知) =====> @After(后置通知) =========> @AfterReturning(正常返回)
* 异常执行:@Before(前置通知) =====> @After(后置通知) =========> @AfterThrowing(方法执行异常)
*/
[Logs][add]方法开始执行,用的参数列表[[2, 13]]
[Logs][add]执行完成
[Logs][add]方法正常执行完成,结果为[15]
使用环绕通知时,方法的输出如下:
方法前置通知
方法正常执行通知
方法结束执行通知
15
环绕通知和其他通知混合时的执行顺序
环绕通知优于其他通知执行。
顺序:
(环绕前置->普通前置)->目标方法执行->环绕正常返回/异常执行->环绕后置->普通后置->普通返回或异常
有时可以普通前置在前面执行。
注意事项
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
多切面时的执行顺序
与栈的顺序一样,先进后出
环绕通知只影响当前切面。
基于配置文件的AOP
在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>
元素内部。对于每个切面而言,都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端bean实例。
eg:
<aop:config>
<!-- 全局切入点-->
<aop:pointcut id="allPointCut" expression="execution(public int com.third.impl.MyMathCalculator.*(int,int))"/>
<!-- 指定切面-->
<aop:aspect ref="logUtils">
<!-- 配置切点表达式,当前切面能用-->
<aop:pointcut id="myPointcut" expression="execution(public int com.third.impl.MyMathCalculator.*(int,int))"/>
<!-- 配置前置方法-->
<aop:before method="logStart" pointcut="execution(public int com.third.impl.MyMathCalculator.*(int,int))"></aop:before>
<!-- 配置正常执行方法,使用外部引入切点-->
<aop:after-returning method="logReturn" pointcut-ref="myPointcut" returning="result"></aop:after-returning>
<!-- 配置异常执行方法-->
<aop:after-throwing method="logException" pointcut-ref="myPointcut" throwing="e"></aop:after-throwing>
<!-- 配置后置通知方法-->
<aop:after method="logEnd" pointcut-ref="myPointcut"></aop:after>
</aop:aspect>
<!-- <aop:aspect ref="validateAspect"></aop:aspect>-->
</aop:config>
事务部分待补充
To be continue…
参考资料
1.https://blog.csdn.net/itcats_cn/article/details/81479185
2.https://www.bilibili.com/video/av71336532/