版本发展节点
工厂(设计模式)+反射(机制) + 配置文件(xml)。
在Spring 3.0之前, 主流使用xml配置 + 注解开发, xml配置开发, 可以帮我们理解代码的本质和底层逻辑, 但随着Spring 3.0的到来, 纯注解开发成为了主流
学习了IOC后, 我们虽然解决了类和类之间的耦合关系, 但需要却在获取对象的时候创建Spring工厂, 那有没有更方便获取对象的依赖的方法呢?
DI注入: Dependency Injection 依赖注入,在Spring框架负责创建Bean对象时,动态的将依赖对象注入到Bean组件(简单的说,可以将另外一个bean对象动态的注入到另外一个bean中。)
由Spring容器创建了Service、Dao对象,并且在配置中将Dao传入Servcie,那么Service对象就包含了Dao对象的引用。
1.将service对象也交给spring容器管理
<bean id="userDao" class="cn.itcast.spring.a_quickstart.UserDaoImpl">bean>
<bean id="userService" class="cn.itcast.spring.a_quickstart.UserServiceImpl">
<property name="userDao" ref="userDao">property>
bean>
2.在程序中定义属性提供setter方法:
public class UserServiceImpl implements IUserService {
private IUserDao userDao;
public void setUserDao(IUserDao userDao) {
this.userDao = userDao;
}
}
一、手动注入
<bean name="userService" class="com.mtb.service.UserService">
<property name="orderService" ref="orderService"/>
bean>
<bean name="userService" class="com.mtb.service.UserService">
<constructor-arg index="0" ref="orderService"/>
bean>
二、自动注入
自动注入也可以分为两种:@Autowired注解注入
和 XML中配置autowire注入(不常见)
这里我们简单聊一下@Autowired注解注入(在后续的篇章中会展开了讲), @Autowired可以作用在属性、构造方法、set方法上
属性类型
去找Bean,如果找到多个再根据属性名
确定一个参数类型
去找Bean,如果找到多个再根据参数名
确定一个参数类型
去找Bean,如果找到多个再根据参数名
确定一个IOC经常会和DI一起提起,有些同学还会把这两者混为一谈。实际上IOC和DI并不是同一个概念。IOC是一种设计模式,DI是IOC的实现方式。
ApplicationContext工厂, 直译为应用上下文, 是用来加载Spring框架配置文件,来构建Spring的工厂对象, 也称之为Spring的容器。
ApplicationContext 只是BeanFactory(Bean工厂,Bean就是一个java对象) 一个子接口:
ApplicationContext 更加强大, 所以现在开发基本没人使用BeanFactory。
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//1.通过spring容器的bean的id/name来获取
IUserService userService = (IUserService) ac.getBean("userService");
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//1.通过spring容器的bean的id/name来获取
IUserService userService2 = (IUserService) ac.getBean(IUserService.class);
AOP 思想: 基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强 !
一、AOP核心概念
二、专业名词
一、底层实现
Spring AOP是基于动态代理的,基于两种动态代理机制:
二、动态代理和静态代理区别?
参考: 说说动态代理与静态代理区别
JDK动态代理,针对目标对象的接口进行代理 ,动态生成接口的实现类 !(必须有接口)
//接口(表示代理的目标接口)
public interface ICustomerService {
//保存
public void save();
//查询
public int find();
}
//实现类
public class CustomerServiceImpl implements ICustomerService{
public void save() {
System.out.println("客户保存了。。。。。");
}
public int find() {
System.out.println("客户查询数量了。。。。。");
return 100;
}
}
//专门用来生成jdk的动态代理对象的-通用
public class JdkProxyFactory{
//成员变量
private Object target;
//注入target目标对象
public JdkProxyFactory(Object target) {
this.target = target;
}
public Object getProxyObject(){
//参数1:目标对象的类加载器
//参数2:目标对象实现的接口
//参数3:回调方法对象
/**方案一:在内部实现new InvocationHandler(),指定匿名类*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//如果是保存的方法,执行记录日志操作
if(method.getName().equals("save")){
writeLog();
}
//目标对象原来的方法执行
Object object = method.invoke(target, args);//调用目标对象的某个方法,并且返回目标对象方法的返回值
return object;
}
});
}
//记录日志
private static void writeLog(){
System.out.println("增强代码:写日志了。。。");
}
}
public static void main(String[] args) {
//target(目标对象)
ICustomerService target = new CustomerServiceImpl();
//实例化注入目标对象
JdkProxyFactory jdkProxyFactory = new JdkProxyFactory(target);
//获取Proxy Object代理对象:基于目标对象类型的接口的类型的子类型的对象
ICustomerService proxy = (ICustomerService) jdkProxyFactory.getProxyObject();
//调用目标对象的方法
proxy.save();
System.out.println("————————————————————————————————————————");
proxy.find();
}
从结果上看出:在保存方法的前面,输入了日志增强。
JDK动态代理的缺点:
只能面向接口代理,不能直接对目标类进行代理 ,如果没有接口,则不能使用JDK代理。
Cglib的引入为了解决类的直接代理问题(生成代理子类),不需要接口也可以代理 !
将spring的核心jar(spring-core)导入进来,因为spring的包,包含了cglib的包
//没有接口的类
public class ProductService {
public void save() {
System.out.println("商品保存了。。。。。");
}
public int find() {
System.out.println("商品查询数量了。。。。。");
return 99;
}
}
//cglib动态代理工厂:用来生成cglib代理对象
public class CglibProxyFactory implements MethodInterceptor{
//声明一个代理对象引用
private Object target;
//注入代理对象
public CglibProxyFactory(Object target) {
this.target = target;
}
//获取代理对象
public Object getProxyObject(){
//1.代理对象生成器(工厂思想)
Enhancer enhancer = new Enhancer();
//2.在增强器上设置两个属性
//设置要生成代理对象的目标对象:生成的目标对象类型的子类型
enhancer.setSuperclass(target.getClass());
//设置回调方法
enhancer.setCallback(this);
// Callback
//3.创建获取对象
return enhancer.create();
}
//回调方法(代理对象的方法)
//参数1:代理对象
//参数2:目标对象的方法对象
//参数3:目标对象的方法的参数的值
//参数4:代理对象的方法对象
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
//如果是保存的方法,执行记录日志操作
if(method.getName().equals("save")){
writeLog();
}
//目标对象原来的方法执行
Object object = method.invoke(target, args);//调用目标对象的某个方法,并且返回目标对象方法的执行结果
return object;
}
//写日志的增强功能
//Advice通知、增强
//记录日志
private static void writeLog(){
System.out.println("增强代码:写日志了。。。");
}
}
//cglib动态代理:可以基于类(无需实现接口)生成代理对象
@Test
public void testCglibProxy(){
//target目标:
ProductService target = new ProductService();
//weave织入,生成proxy代理对象
//代理工厂对象,注入目标
CglibProxyFactory cglibProxyFactory = new CglibProxyFactory(target);
//获取proxy:思考:对象的类型
//代理对象,其实是目标对象类型的子类型
ProductService proxy=(ProductService) cglibProxyFactory.getProxyObject();
//调用代理对象的方法
proxy.save();
System.out.println("————————————————————————————————————————");
proxy.find();
}
控制台输出结果
最后,使用断点查看cglib代理,生成的代理对象
如图:
CustomerServiceImpl(基于接口)的代理对象是$Proxy
ProductService(基于一般类)的代理对象是ProductService的子类
代理知识总结:
Advice通知就是增强的方式方法
传统AOP编程配置过于麻烦,所以这里使用AspectJ的切入点语法(xml配置)来讲解
一、引入导入依赖包
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.10.RELEASEversion>
dependency>
二、编写环绕通知
编写传统aop的Advice通知类。
传统aop的通知,必须实现上面的这5类接口,例如我们编写一个环绕通知,就必须实现MethodInterceptor接口
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;
//传统的aop的advice通知,增强类,必须实现org.aopalliance.intercept.MethodInterceptor接口(注意和cglib代理接口区分开)
public class TimeLogInterceptor implements MethodInterceptor {
//log4j记录器
private static Logger LOG=Logger.getLogger(TimeLogInterceptor.class);
//回调方法
//参数:目标方法回调函数的包装类,获取调用方法的相关属性、方法名、调用该方法的对象(即封装方法、目标对象,方法的参数)
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
//业务:记录目标的方法的运行时间
//方法调用之前记录时间
long beginTime = System.currentTimeMillis();
//目标对象原来的方法的调用,返回目标对象方法的返回值。
Object object = methodInvocation.proceed();//类似于invoke
//方法调用之后记录时间
long endTime = System.currentTimeMillis();
//计算运行时间
long runTime=endTime-beginTime;
//写日志:
/**
* 1:记录日志到数据库(优势:便于查询;劣势:要占用数据库空间,日志一般都非常庞大)
* 2:记录日志到log4j(优势:文件存储,可以记录非常大的日志数据,而且还有日志级别的特点;劣势:不便于查询)
*/
LOG.info("方法名为:"+methodInvocation.getMethod().getName()+"的运行时间为:"+runTime+"毫秒");
return object;
}
}
三、确定切入点
在确定切入点之前, 我们先要在applicationContext.xml中引入aop的文件约束
<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"
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">
<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"
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">
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
<bean id="timeLogAdvice" class="cn.itcast.spring.b_oldaop.TimeLogInterceptor"/>
<aop:config>
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:advisor advice-ref="timeLogAdvice" pointcut-ref="myPointcut"/>
aop:config>
beans>
相比传统Spring AOP通知类型多了 After最终通知 (类似 finally )。
一、确定切入点
我们这把目标对象是name以Service结尾的bean
1.我们创建两个bean, 一个基于接口的实现类, 另一个是一般类, 验证Jdk和Cglib的使用
接口实现类(CustomerServiceImpl)
public class CustomerServiceImpl implements ICustomerService {
public void save() {
System.out.println("客户保存了。。。。。");
}
public int find() {
System.out.println("客户查询数量了。。。。。");
return 100;
}
}
一般类(ProductService)
public class ProductService {
public void save() {
System.out.println("商品保存了。。。。。");
}
public int find() {
System.out.println("商品查询数量了。。。。。");
return 99;
}
}
2.注册到Spring容器中, 取名时都以Service结尾
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
二、编写advice增强
与传统AOP的不同在于, 不用去实现对应的接口(aspectj的advice通知增强类,无需实现任何接口)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//前置通知
//普通的方法。方法名随便,但也不能太随便,一会要配置
public void firstbefore(){
System.out.println("------------第一个个前置通知执行了。。。");
}
}
同样将前置通知配置到spring的容器中
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
三、配置切面
<aop:config>
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:aspect ref="myAspectAdvice">
<aop:before method="firstbefore" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
我们先来了解下xml方式, 是怎么实现的
1.编写通知
我们可以将所有的通知都写到同一个类中,
配置MyAspect类(切面),配置before方法(通知)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//前置通知的:方法运行之前增强
//应用: 权限控制 (权限不足,抛出异常)、 记录方法调用信息日志
//参数:org.aspectj.lang.JoinPoint
//参数:连接点对象(方法的包装对象:方法,参数,目标对象)
public void before(JoinPoint joinPoint){
//分析:抛出异常拦截的
//当前登录用户
String loginName = "Rose";
System.out.println("方法名称:"+joinPoint.getSignature().getName());
System.out.println("目标对象:"+joinPoint.getTarget().getClass().getName());
System.out.println("代理对象:"+joinPoint.getThis().getClass().getName());
//判断当前用户有没有执行方法权限
if(joinPoint.getSignature().getName().equals("save")){
if(!loginName.equals("admin")){
//只有超级管理员admin有权限,其他人不能执行某个方法,比如查询方法
throw new RuntimeException("您没有权限执行方法:"+joinPoint.getSignature().getName()+",类型为:"+joinPoint.getTarget().getClass().getName());
}
}
}
}
2.配置切面(关联切入点和通知)
<bean id="customerService" class="com.itheima.jdk.CustomerServiceImpl"/>
<bean id="productService" class="com.itheima.cglib.ProductService"/>
<bean id="myAspectAdvice" class="com.itheima.aop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:before method="before" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
简约写法
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:before method="before" pointcut="bean(*Service)" />
aop:aspect>
aop:config>
我们测试一下
//springjunit集成测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext-aspectj.xml")
public class SpringTest {
//注入要测试bean
@Autowired
private ICustomerService customerService;
@Autowired
private ProductService productService;
//测试
@Test
public void test(){
//基于接口
customerService.save();
customerService.find();
//基于类的
productService.save();
productService.find();
}
}
1.确定目标对象
确定目标对象(目标对象需要被Spring管理),此处给目标对象设置了id标识
//接口
public interface CustomerService {
//保存
public void save();
//查询
public int find();
}
//实现类
/**
* @Service("customerService")
* 相当于spring容器中定义:
*
*/
@Service("customerService")
public class CustomerServiceImpl implements CustomerService{
public void save() {
System.out.println("客户保存了。。。。。");
}
public int find() {
System.out.println("客户查询数量了。。。。。");
return 100;
}
}
//没有接口的类
/**
* @Service("productService")
* 相当于spring容器中定义:
*
*/
@Service("productService")
public class ProductService {
public void save() {
System.out.println("商品保存了。。。。。");
}
public int find() {
System.out.println("商品查询数量了。。。。。");
return 99;
}
}
2.开启aop的aspectj的自动代理
在applicationContext.xml中开启aop的aspectj的自动代理
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://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">
<context:component-scan base-package="com.itheima"/>
<aop:aspectj-autoproxy/>
beans>
3.编写通知,并加上注解
需要保证通知类被Spring管理, 其次添加@Aspect注解, 当我们开启AspectJ注解自动代理机制后(
之后只需要在通知方法上添加对应的注解即可, 例如@Before(前置通知)
// aspectj的advice通知增强类,无需实现任何接口
@Component("myAspect")
@Aspect
public class MyAspect {
//前置通知的:方法运行之前增强
//应用: 权限控制 (权限不足,抛出异常)、 记录方法调用信息日志
//参数:org.aspectj.lang.JoinPoint
//参数:连接点对象(方法的包装对象:方法,参数,目标对象)
@Before("bean(*Service)")
public void before(JoinPoint joinPoint) {
//分析:抛出异常拦截的
//当前登录用户
String loginName = "Rose";
System.out.println("方法名称:" + joinPoint.getSignature().getName());
System.out.println("目标对象:" + joinPoint.getTarget().getClass().getName());
System.out.println("代理对象:" + joinPoint.getThis().getClass().getName());
System.out.println("-----------------------------------------------------");
//判断当前用户有没有执行方法权限
if (joinPoint.getSignature().getName().equals("save")) {
if (!loginName.equals("admin")) {
//只有超级管理员admin有权限,其他人不能执行某个方法,比如查询方法
throw new RuntimeException("您没有权限执行方法:" + joinPoint.getSignature().getName() + ",类型为:" + joinPoint.getTarget().getClass().getName());
}
}
}
}
4.切入点表达式的两种写法
//前置通知
//相当于:
//@Before("bean(*Service)"):参数值:自动支持切入点表达式或切入点名字
@Before("bean(*Service)")
public void before(JoinPoint joinPoint){
System.out.println("=======前置通知。。。。。");
}
//自定义切入点
//方法名就是切入点的名字
//相当于
@Pointcut("bean(*Service)")
private void myPointcut(){}
//自定义切入点
//方法名就是切入点的名字
//相当于
@Pointcut("bean(*Service)")
private void myPointcut2(){}
//前置通知
//相当于:
//相当于:
@Before("myPointcut()||myPointcut2()")
public void before(JoinPoint joinPoint){
System.out.println("=======前置通知。。。。。");
}
特点:在目标方法运行后,返回值后执行通知增强代码逻辑。
分析: 后置通知可以获取到目标方法返回值,如果想对返回值进行操作,使用后置通知(但不能修改目标方法返回 )
@Component("myAspect")
@Aspect
public class MyAspect {
@AfterReturning(value="bean(*Service)",returning="returnVal")
public void afterReturing(JoinPoint joinPoint, Object returnVal) {
//下发短信:调用运行商的接口,短信猫。。。
System.out.println("-++++++++-后置通知-当前下发短信的方法" + "-尊敬的用户,您调用的方法返回余额为:" + returnVal);
}
}
应用场景
特点:目标执行前后,都进行增强(控制目标方法执行)
@Component("myAspect")
@Aspect
public class MyAspect {
@Around("bean(*Service)")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("---环绕通知-----前");
Object object = proceedingJoinPoint.proceed();
System.out.println("---环绕通知-----后");
return object;
}
}
应用场景
作用:目标代码出现异常,通知执行。记录异常日志、通知管理员(短信、邮件)
@Component("myAspect")
@Aspect
public class MyAspect {
@AfterThrowing(value="bean(*Service)",throwing="ex")
public void afterThrowing(JoinPoint joinPoint , Throwable ex){
System.out.println("---抛出通知。。。。。。"+"抛出的异常信息:"+ex.getMessage());
}
}
应用场景
作用:不管目标方法是否发生异常,最终通知都会执行(类似于finally代码功能)
//最终通知
//拦截所有以ice结尾的bean
@After("bean(*ice)")
public void after(JoinPoint joinPoint){
System.out.println("+++++++++最终通知。。。。。。。");
}
第一步:在CustomerServiceImpl的子类中添加一个新的方法update(),而接口中不要定义update()的方法:
@Service("customerService")
public class CustomerServiceImpl implements CustomerService{
public void save() {
System.out.println("客户保存了。。。。。");
}
public int find() {
System.out.println("客户查询数量了。。。。。");
return 100;
}
//子类扩展方法
public void update(){
System.out.println("客户更新了。。。新增方法。。。");
}
}
第二步:在测试类中调用子类的扩展方法:
//springjunit集成测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class SpringTest {
//注入要测试bean
@Autowired
private CustomerService customerService;
@Autowired
private ProductService productService;
//测试
@Test
public void test(){
//基于接口
customerService.save();
customerService.find();
//基于类的
productService.save();
productService.find();
//扩展方法执行:customerService是一个动态代理对象,原因,该对象是接口的子类型的对象
((CustomerServiceImpl)customerService).update();
}
}
结果发现异常:
为什么会抛出异常呢?原因是代理的目标对象是接口,无法转换为子类。
结局方式:设置 proxy-target-class = true
<aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:config proxy-target-class="true">
aop:config>
一、导入AOP相关坐标
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
dependencies>
二、定义接口和实现类
public interface BookDao {
public void save();
public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
三、定义切入点表达式、配置切面(绑定切入点和通知关系)
通知类需要添加@Aspect注解, 这样Spring才能把当前类标识为一个切面供容器读取, 当然我们还需要让Spring能识别这个注解
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类
@Aspect
public class MyAdvice {
//设置切入点,@Pointcut注解要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
四、在配置类中进行Spring注解包扫描和开启AOP功能
只有添加了@EnableAspectJAutoProxy注解, 才能开启对@Aspect的识别
@Configuration
@ComponentScan("com.itheima")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
测试类和运行结果
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
在测试类中验证代理对象
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
//打印对象的类名
System.out.println(bookDao.getClass());
}
}
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
BookDao bookDao = ctx.getBean(BookDao.class);
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
单例是默认值,如果需要单例对象,则不需要配置scope。
<bean id="bookDao" class="com.itheima.ioc.BookDaoImpl" scope="prototype"/>
//测试生命周期过程中的初始化和销毁bean
@Component("lifeCycleBean")
//@Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Scope("prototype")//默认是单例(singleton),更改为多例(prototype)
public class LifeCycleBean {
}
bean的生命周期有4个阶段
实例化 > 属性赋值 > 初始化 > 销毁
我们可以通过下图来了解这4个阶段分别在什么时候
我们如果想让bean在这四个阶段执行指定的代码, 该如何做呢?*
@Service
public class BookService {
private BookDao bookDao;
public BookService(){
System.out.println("实例化");
}
@Autowired
public void setBookDao(BookDao bookDao) {
System.out.println("属性填充");
this.bookDao = bookDao;
}
//初始化后自动调用方法:方法名随意,但也不能太随便,一会要配置
@PostConstruct
public void init(){
System.out.println("初始化");
}
//bean销毁时调用的方法
@PreDestroy
public void destroy(){
System.out.println("销毁");
}
}
<bean id="bookService" class="cn.itcast.spring.BookService" init-method="init" destroy-method="destroy" />
这两个注解在Java 9中已被弃用, 需要添加依赖才能使用
执行结果如下:
为什么没有调用销毁方法?
我们发现并没有执行销毁方法,这是因为当方法运行结束,jvm直接关了,Spring容器还来不及销毁对象
解决方式:手动销毁spring容器,自动销毁单例的对象
销毁方法的执行必须满足两个条件:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//为什么没有销毁方法调用。
//原因是:使用debug模式jvm直接就关了,spring容器还没有来得及销毁对象。
//解决:手动关闭销毁spring容器,自动销毁单例的对象
ctx.close();
}
实现InitializingBean, DisposableBean接口, 跟添加@PostConstruct、@PreDestroy一样的效果
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
System.out.println("set .....");
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
}
默认使用无参构造方法, 无参构造方法如果不存在,将抛出异常BeanCreationException
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
System.out.println("factory setup....");
return new OrderDaoImpl();
}
}
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>
在执行new ClassPathXmlApplicationContext(“applicationContext.xml”)时,就将所有的对象都生成了,无参构造就调用无参构造函数生成,如果是静态工厂方式就直接通过反射执行工厂类的静态方法,这个过程中是不需要创建工厂类的,所以Spring是单例的,之后获取的bean1都是同一个
//实例工厂创建对象
public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
<!--方式三:使用实例工厂实例化bean-->
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
纯注解实现静态工厂方式
@Bean是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里
Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理。
@Component
public class OrderDaoFactory {
@Bean
public OrderDao getOrderDao(){
System.out.println("factory setup....");
return new OrderDaoImpl();
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
OrderDao book = (OrderDao) ctx.getBean("orderDao");
}
之前讲过BeanFactory(工厂类的顶层接口),而这里是FactoryBean,是两个完全不同的概念
public class Bean1 {
}
//实现FactoryBean的方式
//泛型:你要返回什么类型的对象,泛型就是什么
public class Bean1Factory implements FactoryBean<Bean1> {
public Bean1Factory() {
System.out.println("sss");
}
//用来获取bean的实例对象
public Bean1 getObject() throws Exception {
//写一写初始化数据库连接等代码
System.out.println("ggg");
return new Bean1();
}
public Class<?> getObjectType() {
return null;
}
public boolean isSingleton() {
return false;
}
}
<bean id="bean1" class="cn.itcast.spring.a_quickstart.Bean1Factory"/>
在Bean初始化的前后,对Bean对象进行增强; 它既可以增强一个指定的Bean,也可以增强所有的Bean
底层很多功能(如AOP等)的实现都是基于它的,Spring可以在容器中直接识别调用。
创建后处理类, 需要实现BeanPostProcessor(后置处理器)
@Configuration
public class MyBeanPostProcessor implements BeanPostProcessor{
/**
* 实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务
* 注意:方法返回值不能为null
* 如果返回null那么在后续初始化方法将报空指针异常或者通过getBean()方法获取不到bena实例对象
* 因为后置处理器从Spring IoC容器中取出bean实例对象没有再次放回IoC容器中
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化 before--实例化的bean对象:"+bean+"\t"+beanName);
// 可以根据beanName不同执行不同的处理操作
return bean;
}
/**
* 实例化、依赖注入、初始化完毕时执行
* 注意:方法返回值不能为null
* 如果返回null那么在后续初始化方法将报空指针异常或者通过getBean()方法获取不到bena实例对象
* 因为后置处理器从Spring IoC容器中取出bean实例对象没有再次放回IoC容器中
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化 after...实例化的bean对象:"+bean+"\t"+beanName);
// 可以根据beanName不同执行不同的处理操作
return bean;
}
}
如果要增强一个指定的bean,则只要在方法中根据bean的名称进行处理
spring在初始化MyBeanPostProcessor的时候,判断是否实现了BeanPostProcessor,如果实现了,就采用动态代理的方式,对所有的bean对象增强
方法 | 说明 |
---|---|
postProcessBeforeInitialization | 实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务 |
postProcessAfterInitialization | 实例化、依赖注入、初始化完毕时执行 |
1、处理自定义注解。bean可以添加我们自定义的注解,自定义的注解处理方式在该类中实现,如通过注解识别一组或几组bean,在后续的业务处理中根据组bean进行逻辑。
2、打印日志,将每个bean的初始化情况打印出来;打印初始化时间等。
参考内容: Spring之BeanPostProcessor(后置处理器)介绍
<bean id="car" class="cn.itcast.spring.Car">
<constructor-arg index="0" name="id" value="1"/>
<constructor-arg name="name" >
<value>宝马2代value>
constructor-arg>
<constructor-arg type="java.lang.Double" value="99999d"/>
bean>
注入引用类型的值(参数第一组不变, 用于定位属性, 参数第二组改为ref来指向对象)
/**
* 定义人类
* setter方法属性注入
* 相当于new Person();
*/
public class Person {
private Integer id;
private String name;
private Car car;
//必须提供setter属性方法
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setCar(Car car) {
this.car = car;
}
}
<bean id="person" class="cn.itcast.spring.Person">
<property name="id" value="1001"/>
<property name="name" value="Tom"/>
<property name="car">
<ref bean="car"/>
property>
bean>
Spring2.5版本开始引入了一个新的p名称空间。简单的说,它的作用是为了简化setter方法属性依赖注入配置的,它不是真正的名称空间。
xmlns:p="http://www.springframework.org/schema/p"
<bean id="person" class="cn.itcast.spring.Person" p:id="1002" p:name="关羽" p:car-ref="car"/>
语法: #{…} , 引用另一个Bean 、属性、 方法 , 运算
SpEL表达式的使用功能比较多,Bean操作相关的通常有:
<bean id="person3" class="cn.itcast.spring.e_xmlpropertydi.Person"
p:id="#{1+1}" p:name="#{person.name.toUpperCase()}" p:car="#{car}">bean>
简单数据类型, 我们可以使用@Value来注入值
在属性声明上面注入,底层自动还是生成set方法, 其中customerDao表示节点id的属性值
//@Component(value="customer")
@Service(value="customer")
public class CustomerService {
//在属性声明上面注入,底层自动还是生成setCustomerDao()
//第一种: 使用@Value 结合SpEL ---- spring3.0 后用
//其中customerDao表示节点id的属性值
@Value("#{customerDao}")
private CustomerDao customerDao;
//保存业务方法
public void save(){
System.out.println("CustomerService业务层被调用了。。。");
System.out.println("name:"+name);
customerDao.save();
}
}
单独使用@Autowired
单独使用@Autowired ,表示按照类型注入,会到spring容器中查找CustomerDao的类型,对应
//使用spring的@Autowired
@Autowired//默认按照类型注入
private CustomerDao customerDao;
按照类型注入会遇到的问题:如果IOC容器中同类的Bean有多个,那么默认按照变量名和Bean的名称匹配,建议使用@Qualifier注解指定要装配的bean名称
这时候会发现注入的地方报错 不清楚要注入哪个bean 错误如下图
这时候我们就可以使用@Qualifier来配合@Autowire进行注入了
首先在实例化bean的时候指定名字 注入时使用对应的名字注入 如下图
指定名称为user1的bean
注意事项:
在使用@Autowired时,首先在容器中查询对应类型的bean。如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据如果查询的结果不止一个,那么@Autowired会根据名称来查找。如果查询的结果为空,那么会抛出异常。
参考: @Autowired用法详解
使用@Autowired + @Qualifier
使用@Autowired + @Qualifier表示按照名称注入,回到spring容器中查找customerDao的名称,对应
@Autowired//默认按照类型注入的
@Qualifier("customerDao") //必须配合@Autowired注解使用,根据名字注入
private CustomerDao customerDao;
使用@Resource注解,表示先按照名称注入,会到spring容器中查找customerDao的名称,对应,id的属性值,如果找到,可以匹配。
如果没有找到,则会按照类型注入,会到spring容器中查找CustomerDao的类型,对应
@Resource//默认先按照名称进行匹配,再按照类型进行匹配
private CustomerDao customerDao;
如果@Resource注解上添加name名称,只能按照customerDao名称进行匹配
//第三种: JSR-250标准(jdk) 提供@Resource
@Resource(name="customerDao")//只能按照customerDao名称进行匹配
private CustomerDao customerDao;
之前我们如果要使用JDBC来连接数据库, 要进行六个步骤的操作
而Spring提供了一个更快捷的jdbc操作目标:JdbcTemplate, 用来简化jdbc的操作
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
一、引入依赖包
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.29version>
dependency>
二、代码实现
@Test
public void test(){
//目标:使用jdbctemplate执行一段sql
//1.构建数据源 DriverManagerDataSource
//spring内置了一个数据源
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/my_database?useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("root");
//2.创建jdbctemplate实例
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3.执行sql,创建表test001
jdbcTemplate.execute("create table test001(id int,name varchar(20))");
}
三、存在的问题
上述代码中的dataSource以及jdbcTemplate都需要我们手动创建 ,能不能将这两个对象个交给Spring管理?
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/my_database"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
bean>
参考内容: JdbcTemplate_注解使用方式
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.0.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.16version>
dependency>
<dependency>
<groupId>c3p0groupId>
<artifactId>c3p0artifactId>
<version>0.9.1.2version>
dependency>
@Configuration
public class SpringConfig {
@Bean
public DruidDataSource dataSource() {
// ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);
}
}
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
<context:property-placeholder location="jdbc.properties,msg.properties"/>
<context:property-placeholder location="*.properties"/>
<context:property-placeholder location="classpath:*.properties"/>
<context:property-placeholder location="classpath*:*.properties"/>
Spring3.0开启了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道
传统的开发, 是要手动编写Spring核心配置文件(applicationContext.xml), 而Spring 3.0开启了纯注解的开发模式, 使用java类替代配置文件
//声明当前类为Spring配置类
@Configuration
//Spring注解扫描,相当于
@ComponentScan("com.itheima")
//设置bean扫描路径,多个路径书写为字符串数组格式
//@ComponentScan({"com.itheima.service","com.itheima.dao"})
public class SpringConfig {
}
使用AnnotationConfigApplicationContext读取配置类, 初始化容器
//加载配置文件初始化容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//加载配置类初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
注意:使用注解开发初始化Spring容器时, 使用的是AnnotationConfigApplicationContext, 它是ApplicationContext的子类, 可以通过Ctrl+Alt+Shift+U查看继承关系图
在类上使用@Component注解定义Bean。
@Component("bookDao")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
如果@Component注解没有使用参数指定Bean的名称,那么类名首字母小写就是Bean在IOC容器中的默认名称。例如:BookServiceImpl对象在IOC容器中的名称是bookServiceImpl。
@Component三个衍生注解
使用@Scope定义bean作用范围
@Repository
@Scope("singleton")
public class BookDaoImpl implements BookDao {
}
使用@PostConstruct、@PreDestroy定义bean生命周期
@Repository
@Scope("singleton")
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("book dao constructor ...");
}
@PostConstruct
public void init(){
System.out.println("book init ...");
}
@PreDestroy
public void destroy(){
System.out.println("book destory ...");
}
}
注意:@PostConstruct和@PreDestroy注解是jdk中提供的注解,从jdk9开始,jdk中的javax.annotation包被移除了,也就是说这两个注解就用不了了,可以额外导入一下依赖解决这个问题。
<dependency>
<groupId>javax.annotationgroupId>
<artifactId>javax.annotation-apiartifactId>
<version>1.3.2version>
dependency>
Spring事务管理高层抽象主要包括3个接口,Spring的事务主要是由他们共同完成的:
Spring为不同的持久化框架提供了不同PlatformTransactionManager接口实现:
事务 | 说明 |
---|---|
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用Spring JDBC 或iBatis 进行持久化数据时使用 |
org.springframework.orm.hibernate5.HibernateTransactionManager | 使用Hibernate5.0 版本进行持久化数据时使用 |
org.springframework.orm.jpa.JpaTransactionManager | 使用JPA进行持久化时使用 |
org.springframework.jdo.JdoTransactionManager | 当持久化机制是Jdo时使用 |
org.springframework.transaction.jta.JtaTransactionManager | 使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用 |
我们需要根据项目使用的持久化框架来配置不同的事务管理器(如果是SpringBoot, 则不需要手动定义事务管理器)
一、xml方式
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
二、配置方式
在Spring配置类中,注册事务管理器
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
// HibernateTransactionManager dtm = new HibernateTransactionManager();
DataSourceTransactionManager dtm = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
用户根据选择和使用的持久层技术,来选择对应的事务管理器
我们要了解事务的隔离级别, 首先要搞懂脏读、幻读、不可重复读
一、脏读 (读取未提交数据)
一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
简单理解:事务A读取到事务B修改了但未提交的数据
二、不可重复读 (前后多次读取,数据内容不一致)
事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
简单理解:事务A前后两次读取同一条数据,数据内容不一致, 原因是其他事务对这条数据的内容进行了修改并提交
三、幻读 (前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
简单理解:事务A前后两次读取到的数据总量不一致, 原因是其他事务新增、删除了数据
一、读未提交(Read uncommitted)
在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。
读未提交可能会产生脏读的现象,那么怎么解决脏读呢?那就是使用读已提交。
二、读已提交(Read committed)
在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。
注意:这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。
已提交可能会产生不可重复读的现象,我们可以使用可重复读。
三、可重复读(Repeatable read)
在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。
注意:这才是mysql的默认事务隔离级别
可重复读依然会产生幻读的现象,此时我们可以使用串行化来解决。
四、可串行化(Serializable)
在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
下面用一张表格来表示他们能够解决的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(Read uncommitted) | ×(未解决) | × | × |
读已提交(Read committed) | √(解决) | × | × |
可重复读(Repeatable read) | √ | √ | × |
可串行化(Serializable) | √ | √ | √ |