Inverse of Control(控制反转)
一、作用
将原来在程序中手动创建的对象,交给Spring工厂来创建管理。
二、原理
Spirng工厂通过反射的原理, 根据具体类型的类字符串生产对象的实例,
IoC底层实现:工厂(设计模式) + 反射(机制) + 配置文件(xml)
三、流程
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.10.RELEASEversion>
dependency>
// 接口
public interface BookService {
public void save();
}
// 实现类
public class BookServiceImpl implements BookService{
public void save() {
System.out.println("保存书本信息");
}
}
<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">
<bean id="bookService" class="com.itheima.ioc.BookServiceImpl">bean>
beans>
可参考:Spring_IOC
Dependency Injectio(依赖注入)
一、作用
简单的说,在运行期间, Spring根据配置文件, 通过目标bean提供的setter方法, 可以将另外一个bean的引用动态的注入到目标bean中。
二、原理
通过反射实现注入的,反射允许程序在运行的时候动态的生成对象
三、DI注入的两种方式
一、为属性提供set方法
public class BookServiceImpl implements BookService {
private BookDao bookDao;
/**
* 提供依赖对象对应的setter方法
*/
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
bookDao.save();
}
}
二、aapplicationContext.xml
<bean id="bookDao" class="com.itheima.ioc.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.ioc.BookServiceImpl">
<property name="bookDao" ref="bookDao" />
bean>
一、为属性提供set方法
public class BookServiceImpl implements BookService {
private BookDao bookDao;
/**
* 提供依赖对象对应的setter方法
*/
public BookServiceImpl (BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
bookDao.save();
}
}
二、aapplicationContext.xml
<bean id="bookDao" class="com.itheima.ioc.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.ioc.BookServiceImpl">
<constructor-arg ref="bookDao" index="0">constructor-arg>
bean>
IOC和DI小结
ApplicationContext直译为应用上下文,是用来加载Spring框架配置文件,来构建Spring的工厂对象,它也称之为Spring容器的上下文对象,也称之为Spring的容器。
ApplicationContext 只是BeanFactory(Bean工厂,Bean就是一个java对象)一个子接口:
通过加载Spring核心配置文件(applicationContext.xml)来初始化ApplicationContext(Spring容器)
有以下几种加载方式:
// 1.类路径加载配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2.文件路径加载配置文件
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");
// 3.加载多个配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml", "bean2.xml");
传统的开发, 是要手动编写Spring核心配置文件(applicationContext.xml), 而Spring 3.0开启了纯注解的开发模式, 使用java类替代配置文件, 开启了Spring快速开发的赛道
- @Configuration 注解用于设定当前类为配置类
- @ComponentScan 用于批量注册bean, 类上有@Repository、@Service、@Controller、@Component这几个注解中的任何一个,这个类就会被作为bean注册到spring容器中
二、动手实现
1.新增Book, 添加@Component注解
package cn.itcast.pojo;
import org.springframework.stereotype.Component;
@Component
public class Book {
public void say() {
System.out.println("这是一本书");
}
}
2.创建SpringConfig(Spring配置类)替代Spring核心配置文件(applicationContext.xml)
@Configuration
@ComponentScan("cn.itcast")
public class SpringConfig {
}
3.初始化Spring容器, 并从容器中获取对应的bean, 注意此处使用AnnotationConfigApplicationContext
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
Book book = (Book)ctx.getBean("book");
book.say();
}
注意:使用注解开发初始化Spring容器时, 使用的是AnnotationConfigApplicationContext, 它是ApplicationContext的子类, 可以通过Ctrl+Alt+Shift+U查看继承关系图
@PropertySource注解
引入外部文件一、创建外部文件:jdbc.properties
name=zhangsan
三.此处可以在Spring容器管理的所有对象中, 通过@Value(“${}”)来注入配置文件里面的信息
一、使用bean名称获取
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
注意点:通过@Component注解
扫描到spring容器中的bean会默认将小写开头的类名作为bean的name,供其他地方引用.
例如:BookDao的name则为bookDao
二、 使用bean类型获取
BookDao bookDao = ctx.getBean(BookDao.class);
三、 使用bean名称获取并指定类型
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
单例是默认值,如果需要单例对象,则不需要配置scope。
一、xml方式
<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 {
}
开启了prototype后,每次getBean 返回一个新的实例
打印结果
bean的生命周期有4个阶段:实例化 -> 属性赋值 -> 初始化 -> 销毁
我们可以通过下图来了解这4个阶段分别在什么时候
这里我们说的 Spring Bean 的生命周期主要指的是singleton bean, 对于prototype的bean, Spring容器在创建好交给使用者之后则不会再管理后续的生命周期。
我们如果想让bean在这四个阶段执行指定的代码, 该如何做呢?
这一章节就是来告诉你, 如果在bean初始化和销毁时,去执行指定代码
一、xml配置
1.提供初始化方法、销毁方法
//测试生命周期过程中的初始化和销毁bean
public class LifeCycleBean {
//定义构造方法
public LifeCycleBean() {
System.out.println("LifeCycleBean构造器调用了");
}
//初始化后自动调用方法:方法名随意,但也不能太随便,一会要配置
public void init(){
System.out.println("LifeCycleBean-init初始化时调用");
}
//bean销毁时调用的方法
public void destroy(){
System.out.println("LifeCycleBean-destroy销毁时调用");
}
}
2.applicationContext.xml配置
<bean id="lifeCycleBean" class="cn.itcast.spring.LifeCycleBean" init-method="init" destroy-method="destroy" />
3.测试
@Test
public void test(){
//先获取spring的容器,工厂,上下文
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
//对于单例此时已经被初始化
//获取bean
LifeCycleBean lifeCycleBean=(LifeCycleBean) applicationContext.getBean("lifeCycleBean");
System.out.println(lifeCycleBean);
//为什么没有销毁方法调用。
//原因是:使用debug模式jvm直接就关了,spring容器还没有来得及销毁对象。
//解决:手动关闭销毁spring容器,自动销毁单例的对象
((ClassPathXmlApplicationContext)applicationContext).close();
}
销毁方法的执行必须满足两个条件:
二、实现接口方法
实现InitializingBean, DisposableBean接口
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");
}
}
三、注解方式
//测试生命周期过程中的初始化和销毁bean
@Component("lifeCycleBean")
public class LifeCycleBean {
public LifeCycleBean() {
System.out.println("LifeCycleBean构造器调用了");
}
//初始化后自动调用方法:方法名随意,但也不能太随便,一会要配置
@PostConstruct//初始化的方法
public void init(){
System.out.println("LifeCycleBean-init初始化时调用");
}
//bean销毁时调用的方法
@PreDestroy
public void destroy(){
System.out.println("LifeCycleBean-destroy销毁时调用");
}
}
容器关闭前触发bean的销毁
关闭容器方式:
ConfigurableApplicationContext
接口close()
操作ConfigurableApplicationContext
接口registerShutdownHook()
操作public class AppForLifeCycle {
public static void main( String[] args ) {
//此处需要使用实现类类型,接口类型没有close方法
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
//注册关闭钩子函数,在虚拟机退出之前回调此函数,关闭容器
ctx.registerShutdownHook();
//关闭容器
//ctx.close();
}
}
一、构造方法方式
默认使用无参构造方法, 无参构造方法如果不存在,将抛出异常BeanCreationException
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
二、 静态工厂方式
1.创建工厂类, 提供静态方法
//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
System.out.println("factory setup....");
return new OrderDaoImpl();
}
}
2.applicationContext.xml配置
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>
三、实例工厂方式
1.创建工厂类, 提供实例方法
//实例工厂创建对象
public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
2.applicationContext.xml配置
<!--方式三:使用实例工厂实例化bean-->
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
总结:
作用:在Bean初始化的前后,对Bean对象进行增强; 它既可以增强一个指定的Bean,也可以增强所有的Bean
底层很多功能(如AOP等)的实现都是基于它的,Spring可以在容器中直接识别调用。
一、动手实践
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的名称进行处理
<bean class="com.dpb.processor.MyBeanPostProcessor">bean>
二、后处理bean的执行时机
方法 | 说明 |
---|---|
postProcessBeforeInitialization | 实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务 |
postProcessAfterInitialization | 实例化、依赖注入、初始化完毕时执行 |
三、使用场景:
1、处理自定义注解。bean可以添加我们自定义的注解,自定义的注解处理方式在该类中实现,如通过注解识别一组或几组bean,在后续的业务处理中根据组bean进行逻辑。
2、打印日志,将每个bean的初始化情况打印出来;打印初始化时间等。
BeanPostProcessor
一、构造器参数注入 constructor-arg
//目标,构造器参数注入,new car直接将参数的值直接赋值
public class Car {
private Integer id;
private String name;
private Double price;
//有参构造
public Car(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Car [id=" + id + ", name=" + name + ", price=" + price + "]";
}
}
注入简单类型的值
<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方法属性注入 property
/**
* 定义人类
* 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;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", 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>
三、p名称空间的使用
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"/>
四、spEL表达式的使用
语法: #{…} , 引用另一个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>
一、构造方法方式
spring在创建bean的时候自动调用无参构造器来实例化,相当于new BookServiceImpl()
<bean id="bookService" class="com.itheima.ioc.BookServiceImpl" />
前提:如果类中有其他构造函数,需要提供显式的无参构造函数
二、静态工厂方式
1.创建工厂, 提供静态方法
//静态工厂
public class BeanFactory {
//静态方法,用来返回对象的实例
public static Bean getBean(){
//在做实例化的时候,可以做其他的事情,即可以在这里写初始化其他对象的代码
//Connection conn....
return new Bean();
}
}
2.直接调用静态方法, 不需要实例化工厂类
<bean id="bean" class="com.itheima.ioc.BeanFactory" factory-method="getBean"/>
三、实例工厂方法
1.创建工厂,提供非静态方法
实例工厂(生产实例的方法不是静态)
public class BeanFactory {
public Bean getBean(){
return new Bean();
}
}
2.需要提前实例化工厂类
<bean id="beanFactory" class="com.itheima.ioc.BeanFactory"/>
<bean id="bean" factory-bean="beanFactory" factory-method="getBean"/>
总结:
四、FactoryBean方式(了解)
1.定义UserDaoFactoryBean实现FactoryBean
UserDaoFactoryBean中实例化什么类型的对象泛型就是该类型。
//FactoryBean创建对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
public Class<?> getObjectType() {
return UserDao.class;
}
}
2.applicationContext.xml配置
<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>
使用之前的AppForInstanceUser测试类去运行看结果就行了。注意配置文件中id="userDao"是否重复。
前期准备:导入Spring核心包
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.10.RELEASEversion>
dependency>
第一步:配置注解开启以及注解扫描
引入context 名称空间,使component-scan
第二步:在类上添加@Component
@Component(value="customerService")
public class CustomerService {
//保存业务方法
public void save(){
System.out.println("CustomerService业务层被调用了。。。");
}
}
在配置包扫描的时候,spring会自动开启注解功能,所以,注解开启功能可以不配置。
即去掉: ,因为
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.itcast.spring"/>
beans>
@Componen的子注解
之前我们学习了bean属性的依赖注入(xml配置), 使用xml配置时, 我们可以通过构造方法、set方法来注入值,接下来我们学一下怎么使用配置来完成bean属性的依赖注入
在属性声明上面注入,底层自动还是生成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的时候指定名字 注入时使用对应的名字注入 如下图
二、使用@Autowired + @Qualifier
使用@Autowired + @Qualifier表示按照名称注入,回到spring容器中查找customerDao的名称,对应
@Autowired//默认按照类型注入的
@Qualifier("customerDao") //必须配合@Autowired注解使用,根据名字注入
private CustomerDao customerDao;
使用@Resource注解,表示先按照名称注入,会到spring容器中查找customerDao的名称,对应
如果没有找到,则会按照类型注入,会到spring容器中查找CustomerDao的类型,对应
@Resource//默认先按照名称进行匹配,再按照类型进行匹配
private CustomerDao customerDao;
如果@Resource注解上添加name名称,只能按照customerDao名称进行匹配
//第三种: JSR-250标准(jdk) 提供@Resource
@Resource(name="customerDao")//只能按照customerDao名称进行匹配
private CustomerDao customerDao;
四、@Inject
需要先导入 javax.inject 的 jar ,在课前资料中查找。
使用@Inject注解,则按照类型注入
//第四种: JSR-330标准(jdk) 提供 @Inject ,配合@name注解
@Inject//默认按照类型注入
private CustomerDao customerDao;
使用@inject和@Named注解,则按照名称注入
//第四种: JSR-330标准(jdk) 提供 @Inject ,配合@name注解
@Inject//默认按照类型注入
@Named("customerDao")//按照名字注入,必须配合@Inject使用
private CustomerDao customerDao;
@Service
public class HelloService {
public void sayHello(){
System.out.println("嘿,传智播客。。。。");
}
}
@WebServlet("hello")
public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//传统方式:
//new service
//spring方式:只要看到new,你就想到spring容器中的
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloService helloService=(HelloService)applicationContext.getBean("helloService");
helloService.sayHello();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
启动项目,并访问
一、手动创建Spring容器的缺点
直接new ClassPathXmlApplicationContext()有什么缺点?
如果每次访问都要初始化Spring容器,消耗了资源,降低了性能
二、解决思路:保证容器对象只有一个。
解决方案:将Spring容器绑定到Web Servlet容器上,让Web容器来管理Spring容器的创建和销毁。
ServletContext在Web服务运行过程中是唯一.它是在WEB服务器启动时,会为每一个WEB应用程序(webapps下的每个目录就是一个应用程序)创建一块共享的存储区域,是所有的Servlet和JSP都可以共享同一个区域。
具体操作:
ServletContext在Web服务运行过程中是唯一的, 其初始化的时候,会自动执行ServletContextListener 监听器
编写一个ServletContextListener监听器,在监听ServletContext到创建的时候,创建Spring容器,并将其放到ServletContext的属性中保存(setAttribute(Spring容器名字,Spring容器对象) )。
我们无需手动创建该监听器,因为Spring提供了一个叫ContextLoaderListener的监听器,它位于spring-web-4.2.4.RELEASE.jar中。
一、导入依赖包
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webartifactId>
<version>5.2.10.RELEASEversion>
dependency>
二、配置监听器
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
ContextLoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。
ContextLoaderListener默认加载的是WEB-INF/applicationContext.xml,如果applicationContext.xml不在这个文件夹下,就会报
解决方案:需要在web.xml中配置,加载spring容器applicationContext.xml文件的路径
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:applicationContext.xmlparam-value>
context-param>
ContextLoaderListener详解
方式一:使用getAttribute
//每次获取的都是一个spring容器
ApplicationContext applicationContext =
(ApplicationContext)this.getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
方式二:使用WebApplicationContextUtils (推荐)
//每次获取的都是一个spring容器
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
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>
通过JoinPoint 连接点对象,获取目标对象信息 !(org.aspectj.lang.JoinPoint)
一、编写通知
我们可以将所有的通知都写到同一个类中,
配置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());
}
}
}
}
二、配置切面(关联切入点和通知)
<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();
}
}
一、确定目标对象
确定目标对象(目标对象需要被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;
}
}
二、开启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>
三、编写通知,并加上注解
需要保证通知类被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());
}
}
}
}
四、切入点表达式的两种写法
//前置通知
//相当于:
//@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("=======前置通知。。。。。");
}
特点:在目标方法运行后,返回值后执行通知增强代码逻辑。
分析: 后置通知可以获取到目标方法返回值,如果想对返回值进行操作,使用后置通知(但不能修改目标方法返回 )
一、配置通知
配置afterReturing方法(通知)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//应用场景:与业务相关的,如网上营业厅查询余额后,自动下发短信。
//后置通知:会在目标方法执行之后调用通知方法增强。
//参数1:连接点对象(方法的包装对象:方法,参数,目标对象)
//参数2:目标方法执行后的返回值,类型是object,“参数名”随便,但也不能太随便,一会要配置
public void afterReturing(JoinPoint joinPoint,Object returnVal){
//下发短信:调用运行商的接口,短信猫。。。
System.out.println("-++++++++-后置通知-当前下发短信的方法"+"-尊敬的用户,您调用的方法返回余额为:"+returnVal);
}
}
二、配置切面
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:after-returning method="afterReturing" returning="returnVal" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
需要开启aop的aspectj的自动代理,
@Component("myAspect")
@Aspect
public class MyAspect {
@AfterReturning(value="bean(*Service)",returning="returnVal")
public void afterReturing(JoinPoint joinPoint, Object returnVal) {
//下发短信:调用运行商的接口,短信猫。。。
System.out.println("-++++++++-后置通知-当前下发短信的方法" + "-尊敬的用户,您调用的方法返回余额为:" + returnVal);
}
}
特点:目标执行前后,都进行增强(控制目标方法执行)
增强代码的方法要求:
第一步:配置MyAspect类(切面),配置around方法(通知)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//应用场景:日志、缓存、权限、性能监控、事务管理
//环绕通知:在目标对象方法的执行前+后,可以增强
//参数:可以执行的连接点对象ProceedingJoinPoint(方法),特点是调用proceed()方法可以随时随地执行目标对象的方法(相当于目标对象的方法执行了)
//必须抛出一个Throwable
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//目标:事务的控制:
//开启事务:
System.out.println("-----开启了事务。。。。。。。。。");
//执行了目标对象的方法
Object resultObject = proceedingJoinPoint.proceed();
//结束事务
System.out.println("-----提交了事务。。。。。。。。。");
return resultObject;//目标对象执行的结果
}
}
第二步:Spring容器中配置,配置applicationContext-aspectJ.xml
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:around method="around" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
@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;
}
}
作用:目标代码出现异常,通知执行。记录异常日志、通知管理员(短信、邮件)
第一步:配置MyAspect类(切面),配置aterThrowing方法(通知)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//作用:目标代码出现异常,通知执行。记录异常日志、通知管理员(短信、邮件)
//只有目标对象方法抛出异常,通知才会执行
//参数1:静态连接点(方法对象)
//参数2:目标方法抛出的异常,参数名随便,但也不能太随便
public void afterThrowing(JoinPoint joinPoint,Throwable ex){
//一旦发生异常,发送邮件或者短信给管理员
System.out.println("++管理员您好,"+joinPoint.getTarget().getClass().getName()+"的方法:"
+joinPoint.getSignature().getName()+"发生了异常,异常为:"+ex.getMessage());
}
}
第二步:Spring容器中配置,配置applicationContext-aspectJ.xml
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:after-throwing method="aterThrowing" throwing="ex" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
在ProductService.java中save的方法中,制造异常:
//没有接口的类
public class ProductService {
public void save() {
System.out.println("商品保存了。。。。。");
//故意制造异常
int d = 1/0;
}
public int find() {
System.out.println("商品查询数量了。。。。。");
return 99;
}
}
只有当切入点发生异常时,才会执行
@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代码功能)
//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
//应用场景:释放资源 (关闭文件、 关闭数据库连接、 网络连接、 释放内存对象 )
//最终通知:不管是否有异常都会执行
public void after(JoinPoint joinPoint){
//释放数据库连接
System.out.println("数据库的connection被释放了。。。。。,执行的方法是:"+joinPoint.getSignature().getName());
}
}
第二步:Spring容器中配置,配置applicationContext-aspectJ.xml
<bean id="customerService" class="cn.itcast.spring.a_proxy.CustomerServiceImpl"/>
<bean id="productService" class="cn.itcast.spring.a_proxy.ProductService"/>
<bean id="myAspectAdvice" class="cn.itcast.spring.c_aspectjaop.MyAspect"/>
<aop:config>
<aop:aspect ref="myAspectAdvice">
<aop:pointcut expression="bean(*Service)" id="myPointcut"/>
<aop:after method="after" pointcut-ref="myPointcut"/>
aop:aspect>
aop:config>
//最终通知
//拦截所有以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>