文章目录
- Spring框架
- Spring入门
- Spring第一个程序
- 启用Log4j2日志框架
- Spring对IoC的实现
- set注入
- 构造注入
- set注入专题
- p命名空间注入
- c命名空间注入
- util命名空间
- 基于XML的自动装配
- spring引入外部属性配置文件
- Bean的作用域
- GoF之工厂模式
- 工厂模式的三种形态
- Bean的获取方式
- Bean的生命周期
- Bean生命周期之5步
- Bean生命周期之7步
- Bean生命周期之10步
- 让Spring管理程序员new的对象
- Bean的循环依赖问题
- 反射机制
- Spring IoC注解开发
- 自定义注解与使用
- 声明Bean的注解
- Spring 注解的使用
- 选择性实例化Bean
- 负责注入的注解
- 全注解式开发
- GoF之代理模式
- 面向切面编程AOP
- 使用Spring的AOP
- 基于AspectJ的AOP注解式开发
- 全注解方式的AOP
Spring框架
举例:代码中分别实现了三个类,其中UserAction属于表示层,UserService表示业务层,UserDao表示持久层。
- 表示层——用户交互的界面。
- 业务层——是表示层和持久层之间沟通的桥梁,主要负责数据的传递和处理。
- 持久层——也称为数据访问层,这一层其实就是跟数据库直接打交道的层面,通过连接数据库,根据传入的值对数据库进行增删改查。
public class UserAction {private UserService userService = new UserServiceImpl();/*** 删除用户信息的请求*/public void deleteRequest(){userService.deleteUser();}
}
public interface UserService {/*** 删除用户信息*/void deleteUser();
}
public class UserServiceImpl implements UserService {UserDao userDao = new UserDaoImplForMySQL();@Overridepublic void deleteUser() {userDao.deleteById();// 处理业务的代码……}}
public interface UserDao
{/*** 根据id删除用户信息*/void deleteById();
}public class UserDaoImplForMySQL implements UserDao {@Overridepublic void deleteById() {System.out.println("MySQL正在删除用户信息……");}
}
OCP原则(开闭原则)
OCP原则是软件开发七大原则中最基本的原则,开指的是扩展开放,闭指的是修改关闭。该原则是最核心、最基本的,其他的六个原则都是为这个原则服务。
**OCP核心:**在扩展系统功能的时候,没有修改以前写好的代码,则表示符合OCP原则。
当进行系统功能扩展的时候,如果修改了之前稳定的程序,则之前所有的程序都需要进行重新测试,造成很多麻烦。
DIP原则(依赖倒置原则)
上图中UserAction中通过实例化UserServiceImpl进行业务操作,而UserServiceImpl实例化UserDaoImplForMySQL进行数据操作。UserServiceImpl是依赖UserDaoImplForMySQL,同时UserAction依赖UserServiceImpl,因此这发个程序违背了依赖依赖倒置原则(下面一动,上面就会受到牵连)。
依赖倒置原则的核心: 倡导面向接口编程,面向抽象编程,不要面向具体编程。
以上举例的程序既违背了OCP原则,也违背了DIP原则,可以使用控制反转的编程思想解决问题。
控制反转(IoC Inversion of Control)
控制反转思想:
- 不在程序中采用硬编码的方式来new对象;
- 不在程序中采用硬编码的方式维护对象的关系。
控制反转是一种编程思想,或者叫做一种新型的设计模式,但由于出现的比较新,所以没有被纳入GoF23种设计模式范围内。
Spring框架的作用:
- Spring框架实现了控制反转IoC这种思想,可以帮我们创建对象,同时帮我们维护对象之间的关系;
- Spring是一种实现了Ioc思想的容器;
- Spring IoC容器的实现原理:工厂模式+解析XML+反射机制
- 控制反转的实现有很多种,其中比较重要的叫做:依赖注入(Dependency Injection,简称DI),依赖注入DI包括常见的两种范式:
- set注入(执行set方法给属性赋值)
- 构造方法注入(执行构造方法给属性赋值)
- 依赖指A对象和B对象的关系;注入是一种手段,通过这种手段可以使A和B产生关系。
Spring的8大模块:
- Spring Core模块:是Spring最基础的部分,它提供了依赖注入特征来实现容器对Bean(每一个被Spring框架管理的对象都被称为Bean,能够放Bean的东西就被称为容器)的管理。核心容器的主要组件是BeanFactory,BeanFactory是工厂模式的一个实现没事任何Spring应用的核心。它使用IoC将应用配置和依赖从实际的应用代码中分离出来。
- Spring Context:是Spring成为框架的原因。这个模块扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。
- Spring AOP模块:提供了对面向切面编程的丰富支持,为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。
- Spring DAO模块:提供了一个JDBC的抽象层和异常层次结构,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。
- Spring ORM模块:Spring并不试图实现它自己的ORM解决方案,而是为几种流行的ORM框架提供了集成方案,包括Hibernate、JDO和iBATIS SQL映射,这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
- Spring Web MVC模块:Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成,例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。
- Spring WebFlux模块:是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。
Spring入门
Spring第一个程序
创建模块(module),并对pom.xml进行以下配置:
1)配置仓库:
<!-- 配置多个仓库,spring里程碑的仓库--><repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled></snapshots></repository></repositories>
2)设置依赖:
<dependencies><!--Spring context 依赖--><!-- 当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了--><!--如果,想使用spring的jdbc,或者说其他的tx,那么还需要再次添加依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.1.0-M2</version></dependency><!-- https://mvnrepository.com/artifact/junit/junit --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency></dependencies>
刷新后,maven中会引入我们在pom.xml中配置的依赖:
并在java中创建包——com.powernode.spring6.bean,并添加User类
随后,在main下的resources文件中添加配置文件——命名为spring:
然后IDEA会自动生成以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--这是spring的配置文件--><!--要按照这个模板创建-->
</beans>
这个文件不一定是spring。这个文件最好放在类路径当中,方便后期的移植,放在resources根目录(resources是类的根路径)下,就相当于放到了类的根路径下。配重bean,这样spring才能帮助我们管理这个对象。
bean标签有两个重要属性:
- id:唯一的标识这个bean;
- class:必须填写类的全路径,全限定类名。(带包名的类名)
当前配置文件如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id = "userBean" class="com.powernode.spring6.bean.User" />
</beans>
添加测试文件:
package com.powernode.spring6.test;import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class FirstSpringTest {@Testpublic void testFirstSpringCode(){// 第一步:获取Spring容器对象// ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring配置文件的路径");//下行代码只要一执行,就相当于启动spring容器,并且解析spring.xml文件,然后将里面的bean的对象全new出来,放在spring容器中。ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");// 第二步:根据bean的id从Spring容器中获取这个对象//applicationContext.getBean("bean的id")Object userBean= applicationContext.getBean("userBean");System.out.println(userBean);}
}
ApplicationContext 翻译为:应用上下文,其实就是Spring容器
ApplicationContext 是一个接口,他有很多实现类,其中一个实现类叫做ClassPathXmlApplicationContext
ClassPathXmlApplicationContext专门从类路径文件加载spring配置文件的一个对象。
以上代码的输出结果:
Spring实例化对象:默认情况下,Spring会通过反射机制,调用类的无参构造方法来实例化对象;若只定义了一个有参数构造函数,则会报错,因此要把无参构造函数给加上。
创建好的对象的存储:Spring创建后对象后,会以一个Map结构的形式存储对象,如:
同时运行多个配置文件:
启用Log4j2日志框架
1)引入Log4j2的依赖:在pom.xml中添加相关依赖,并重新加载。
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency>
2)在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放在类根路径下)
(我在网上随便找了一个)
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数(最小是5秒钟)-->
<configuration monitorInterval="5" status="warn"><!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --><!--变量配置--><Properties><!-- 格式化输出:%date表示日期(可缩写成%d,后同),%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符--><!-- %logger{36} 表示 Logger 名字最长36个字符 --><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss,SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)}: %msg%n" /><!-- 定义日志存储的路径 --><property name="FILE_PATH" value="log" /><!--<property name="FILE_NAME" value="myProject" />--></Properties><!--此节点有三种常见的子节点:Console,RollingFile,File--><appenders><!--console节点用来定义输出到控制台的Appender--><!--target:SYSTEM_OUT或SYSTEM_ERR,一般只设置默认:SYSTEM_OUT--><console name="Console" target="SYSTEM_OUT"><!--输出日志的格式,默认为:%m%n,即只输出日志和换行--><PatternLayout pattern="${LOG_PATTERN}"/><!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/></console><!-- <!–文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用–>--><!-- <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">--><!-- <PatternLayout pattern="${LOG_PATTERN}"/>--><!-- </File>--><!-- 这个会打印出所有的debug及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--><RollingFile name="RollingFileDebug" fileName="${FILE_PATH}/debug.log" filePattern="${FILE_PATH}/debug/DEBUG-%d{yyyy-MM-dd}_%i.log.gz"><!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/><!--如果配置的是“%d{yyyy-MM}”,滚动时间单位就是月。“%d{yyyy-MM-dd}”,滚动时间单位就是天--><PatternLayout pattern="${LOG_PATTERN}"/><!--指定滚动日志的策略,就是指定新建日志文件的时机--><Policies><!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动--><TimeBasedTriggeringPolicy interval="1" modulate="true" /><SizeBasedTriggeringPolicy size="100MB"/></Policies><!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--><DefaultRolloverStrategy max="15"><!--删除15天之前的日志--><Delete basePath="${FILE_PATH}" maxDepth="2"><IfFileName glob="*/*.log.gz" /><IfLastModified age="360H" /></Delete></DefaultRolloverStrategy></RollingFile><!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--><RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/info/INFO-%d{yyyy-MM-dd}_%i.log.gz"><!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/><PatternLayout pattern="${LOG_PATTERN}"/><Policies><!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动--><TimeBasedTriggeringPolicy interval="1" modulate="true" /><SizeBasedTriggeringPolicy size="100MB"/></Policies><!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--><DefaultRolloverStrategy max="15"/></RollingFile><!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--><RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/error/ERROR-%d{yyyy-MM-dd}_%i.log.gz"><!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/><PatternLayout pattern="${LOG_PATTERN}"/><Policies><!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动--><TimeBasedTriggeringPolicy interval="1" modulate="true" /><SizeBasedTriggeringPolicy size="100MB"/></Policies><!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--><DefaultRolloverStrategy max="15"/></RollingFile><!--启用异步日志,阻塞队列最大容量为20000,超出队列容量时是否等待日志输出,不等待将直接将日志丢弃--><Async name="Async" bufferSize="20000" blocking="true"><AppenderRef ref="Console"/><AppenderRef ref="RollingFileDebug"/><AppenderRef ref="RollingFileInfo"/><AppenderRef ref="RollingFileError"/></Async></appenders><!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。--><!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--><loggers><!--过滤掉spring和mybatis的一些无用的DEBUG信息--><logger name="org.mybatis" level="info" additivity="false"><AppenderRef ref="Async"/></logger><!--监控系统信息--><!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。--><Logger name="org.springframework" level="info" additivity="false"><AppenderRef ref="Async"/></Logger><!--root 节点用来指定项目的根日志,level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.--><root level="debug"><AppenderRef ref="Async" /></root></loggers></configuration>
根据视频整了个简易版:
<?xml version="1.0" encoding="UTF-8"?>
<configuration><loggers><root level="DEBUG"><appender-ref ref="spring6log" /></root></loggers><appenders><console name="spring6log" target="SYSTEM_OUT"><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-3level %logger{1024} - %msg%n"/></console></appenders>
</configuration>
3)使用日志框架
package com.powernode.spring6.test;import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class FirstSpringTest {@Testpublic void testLog4j2(){// 自己使用log4j2记录日志信息// 1、创建日志记录器对象// 获取的是FirstSpringTest类的日志记录器对象,也就是说只要是FirstSpringTest类中的代码执行记录日志的话,就输出相关的日志信息。Logger logger = LoggerFactory.getLogger(FirstSpringTest.class);// 2、记录日志,根据不同的级别输出日志logger.info("这是一条消息");logger.info("这是一条调试消息");logger.info("这是一条错误消息");}
}
输出结果:
Spring对IoC的实现
依赖注入实现了控制反转的思想,Spring是通过依赖注入的方式来完成Bean管理的。Bean管理指的是——Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
依赖注入:依赖值得是对象和对象之间的关联关系;注入值得是一种数据的传递行为,通过注入行为让对象和对象产生关系。
依赖注入常见的实现方式包括两种:set注入和构造注入
set注入
set注入是基于set方法实现的,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求属性必须对外提供set方法。
举例:
1)创建模块spring6-003-dependency-injection 并设置pom.xml文件和log4j2.xml
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xxx</groupId><artifactId>spring6-003-dependency-injection</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled></snapshots></repository></repositories><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.1.0-M2</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency></dependencies><properties><maven.compiler.source>18</maven.compiler.source><maven.compiler.target>18</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>
log4j2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration><loggers><!--level:日志输出级别: All < Trace < Debug < Info < Warn < Error < Fatal < OFF--><root level="INFO"><appender-ref ref="spring6log" /></root></loggers><appenders><console name="spring6log" target="SYSTEM_OUT"><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-3level %logger{1024} - %msg%n"/></console></appenders>
</configuration>
2)添加持久模块和业务模块
UserDao.java:
package com.xxx.spring6.dao;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class UserDao {private static final Logger logger = LoggerFactory.getLogger(UserDao.class);public void insert(){//System.out.println("数据库正在保存用户信息……");// 使用kog4j2日志框架logger.info("数据库正在保存用户信息……");}}
UserService.java:
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;public class UserService {private UserDao userDao;// set注入的话,必须提供一个set方法// Spring容器会调用这个set方法,来给userDao属性赋值// 自定义set方法,不符合javabean规范,但至少这个方法要以set单词开始(前三个字母不能随便写)public void setMySQLUserDao(UserDao userDao) {this.userDao = userDao;}// 以下方法是由IDEA生成的,复合javabean规范//public void setUserDao(UserDao userDao) {// this.userDao = userDao;//}public void saveUser(){//保存用户信息到数据库userDao.insert();}}
一般用IDEA自动生成的方法,更规范,这里写自定义方法是举一个例子。
3)添加spring.xml即Spring配置文件:
property中调用set方法,并传递参数。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--配置dao--><bean id="userDaoBean" class="com.xxx.spring6.dao.UserDao"/><!--配置service--><bean id="userServiceBean" class="com.xxx.spring6.service.UserService"><!--name的值是定义的set方法的方法名(去掉set并将剩下的第一个字母小写)--><!--ref翻译为引用,指定的是要注入的bean的id--><property name="mySQLUserDao" ref="userDaoBean"/></bean></beans>
4)添加测试文件
SpringDITest.java:
package com.xxx.spring6.test;import com.xxx.spring6.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class SpringDITest {@Testpublic void testSetDI(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");UserService userService = applicationContext.getBean("userServiceBean", UserService.class);userService.saveUser();}}
测试结果:
构造注入
核心原理:通过调用构造方法来给属性赋值。
在set注入的代码基础上进行修改:
1)添加了一个持久层的类:
package com.xxx.spring6.dao;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class VipDao {private static final Logger logger = LoggerFactory.getLogger(VipDao.class);public void insert(){//System.out.println("数据库正在保存用户信息……");// 使用kog4j2日志框架logger.info("数据库正在保存VIP用户信息……");}}
2)业务层新建一个CustomerService:使用构造函数给Dao中的类赋值
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import com.xxx.spring6.dao.VipDao;public class CustomerService {private UserDao userDao;private VipDao vipDao;public CustomerService(UserDao userDao, VipDao vipDao) {this.userDao = userDao;this.vipDao = vipDao;}public void save(){userDao.insert();vipDao.insert();}
}
3)重新写了一个配置文件:spring2.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userDaoBean" class="com.xxx.spring6.dao.UserDao"/><bean id="vipDaoBean" class="com.xxx.spring6.dao.VipDao"/><!--构造注入--><bean id="csBean" class="com.xxx.spring6.service.CustomerService"><!--index属性指定参数下标,第一个参数是0,第二个参数是1;ref指定用来注入bean的id--><!--指定构造方法的第一个参数,下标是0--><constructor-arg index="0" ref="userDaoBean"/><!--指定构造方法的第二个参数,下标是1--><constructor-arg index="1" ref="vipDaoBean"/></bean><!--方式2 使用name(指向构造函数的参数名)--><bean id="xxx" class="com.xxx.spring6.dao.UserDao"/><bean id="yyy" class="com.xxx.spring6.dao.VipDao"/><bean id="csBean2" class="com.xxx.spring6.service.CustomerService"><constructor-arg name="userDao" ref="xxx"/><constructor-arg name="vipDao" ref="yyy"/></bean><!--方式3 不指定下标,也不指定参数名,让spring自己参数匹配--><!--这种方式实际上是根据类型注入的--><bean id="csBean3" class="com.xxx.spring6.service.CustomerService"><constructor-arg ref="yyy"/><constructor-arg ref="xxx"/></bean></beans>
4)添加测试文件:
package com.xxx.spring6.test;import com.xxx.spring6.service.CustomerService;
import com.xxx.spring6.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class SpringDITest {@Testpublic void testConstructorDI(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");CustomerService customerService = applicationContext.getBean("csBean",CustomerService.class);customerService.save();CustomerService customerService1 =applicationContext.getBean("csBean2",CustomerService.class);customerService1.save();CustomerService customerService2 =applicationContext.getBean("csBean3",CustomerService.class);customerService2.save();}
}
测试结果:
set注入专题
外部Bean和内部Bean
外部Bean:指的是在使用set注入时,通过在property标签中的ref来指定注入对象;
内部Bean:指的是在使用set注入时,通过在property标签中再添加一个bean标签,用来指定被注入的对象;
举例如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--声明/定义Bean--><bean id="orderDaoBean" class="com.xxx.spring6.dao.OrderDao"/><bean id="orderServiceBean" class="com.xxx.spring6.service.OrderService"><!--使用ref属性来引入,这就是外部Bean--><property name="orderDao" ref="orderDaoBean"/></bean><bean id="orderServiceBean2" class="com.xxx.spring6.service.OrderService"><!--在property里面使用嵌套的bean,这就是内部bean--><property name="orderDao"><bean class="com.xxx.spring6.dao.OrderDao"/></property></bean></beans>
注入简单类型
当给Bean注入简单类型时,和注入Bean对象类似,不同在于——注入复杂的Bean对象时,需要ref指向注入对象;而注入简单类型的数据时,使用的是value进行赋值。
例如:添加一个Use对象:该对象的属性都是简单类型的数据,并提供修改属性值的set方法。
package com.xxx.spring6.bean;public class User {private String userName; //String是简单类型private String pswd;private int age;//int是简单类型public void setUserName(String userName) {this.userName = userName;}public void setPswd(String pswd) {this.pswd = pswd;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"userName='" + userName + '\'' +", pswd='" + pswd + '\'' +", age=" + age +'}';}
}
在配置文件进行注入:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--注入简单类型--><bean id="userBean" class="com.xxx.spring6.bean.User"><!--如果是简单类型赋值,就不能是ref,需要使用value--><property name="userName" value="李四"/><property name="pswd" value="666"/><property name="age" value="20"/></bean></beans>
测试:
@Testpublic void testSetSimple(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");User user =applicationContext.getBean("userBean",User.class);System.out.println(user);}
查看Spring框架下的简单类型:
1)Ctrl+N搜索BeanUtils,进入到该类中:
2)Ctrl+f12 输入isSimple:
找到它:
然后按住Ctrl键,再点击return中的isSimpleValueType,跳转到该方法中:
public static boolean isSimpleValueType(Class<?> type) {return (Void.class != type && void.class != type &&(isPrimitiveOrWrapper(type) ||Enum.class.isAssignableFrom(type) ||CharSequence.class.isAssignableFrom(type) ||Number.class.isAssignableFrom(type) ||Date.class.isAssignableFrom(type) ||Temporal.class.isAssignableFrom(type) ||ZoneId.class.isAssignableFrom(type) ||TimeZone.class.isAssignableFrom(type) ||File.class.isAssignableFrom(type) ||Path.class.isAssignableFrom(type) ||Charset.class.isAssignableFrom(type) ||Currency.class.isAssignableFrom(type) ||InetAddress.class.isAssignableFrom(type) ||URI.class == type ||URL.class == type ||UUID.class == type ||Locale.class == type ||Pattern.class == type ||Class.class == type));}
注意:如果把时间作为简单数据类型,使用value赋值的话,这个日期字符串格式有要求,例如:
这样比较麻烦,所以在实际开发中,一般不会把Date作为一个简单,而是通过ref进行赋值。
注入数组
以下的代码都来自教程
数组的元素是简单类型:
Person
package com.xxx.spring6.beans;import java.util.Arrays;public class Person {private String[] favariteFoods;public void setFavariteFoods(String[] favariteFoods) {this.favariteFoods = favariteFoods;}@Overridepublic String toString() {return "Person{" +"favariteFoods=" + Arrays.toString(favariteFoods) +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="person" class="com.xxx.spring6.beans.Person"><property name="favariteFoods"><array><value>鸡排</value><value>汉堡</value><value>鹅肝</value></array></property></bean>
</beans>
当数组中的元素是非简单类型:一个订单中包含多个商品
Goods
package com.xxx.spring6.beans;public class Goods {private String name;public Goods() {}public Goods(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "Goods{" +"name='" + name + '\'' +'}';}
}
Order
package com.xxx.spring6.beans;import java.util.Arrays;public class Order {// 一个订单中有多个商品private Goods[] goods;public Order() {}public Order(Goods[] goods) {this.goods = goods;}public void setGoods(Goods[] goods) {this.goods = goods;}@Overridepublic String toString() {return "Order{" +"goods=" + Arrays.toString(goods) +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="goods1" class="com.xxx.spring6.beans.Goods"><property name="name" value="西瓜"/></bean><bean id="goods2" class="com.xxx.spring6.beans.Goods"><property name="name" value="苹果"/></bean><bean id="order" class="com.xxx.spring6.beans.Order"><property name="goods"><array><!--这里使用ref标签即可--><ref bean="goods1"/><ref bean="goods2"/></array></property></bean></beans>
注入List、Set、Map、Properties
People:
package com.xxx.spring6.bean;import java.util.List;public class People {private List<String> names;private Set<String> phones;private Map<Integer, String> addrs;// Properties的key和value只能是String类型private Properties properties; //Properties本质上也是一个Map集合public void setNames(List<String> names) {this.names = names;}public void setPhones(Set<String> phones) {this.phones = phones;}public void setAddrs(Map<Integer, String> addrs) {this.addrs = addrs;}public void setProperties(Properties properties) {this.properties = properties;}@Overridepublic String toString() {return "People{" +"names=" + names +", phones=" + phones +", addrs=" + addrs +", properties=" + properties +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="peopleBean" class="com.xxx.spring6.bean.People"><property name="names"><list><!--有序可重复--><value>铁锤</value><value>张三</value><value>张三</value><value>张三</value><value>狼</value></list></property><property name="phones"><set><!--无序不可重复--><!--非简单类型可以使用ref,简单类型使用value--><value>110</value><value>110</value><value>120</value><value>120</value><value>119</value><value>119</value></set></property><property name="addrs"><map><!--如果key不是简单类型,使用 key-ref 属性--><!--如果value不是简单类型,使用 value-ref 属性--><entry key="1" value="北京大兴区"/><entry key="2" value="上海浦东区"/><entry key="3" value="深圳宝安区"/></map></property><property name="properties"><props><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></props></property></bean></beans>
测试
@Testpublic void testArray(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array.xml");People peopleBean = applicationContext.getBean("peopleBean", People.class);System.out.println(peopleBean);}
输出:
People{names=[铁锤, 张三, 张三, 张三, 狼], phones=[110, 120, 119], addrs={1=北京大兴区, 2=上海浦东区, 3=深圳宝安区}, properties={password=123456, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root}}
注入null和空字符串
Vip:
package com.xxx.spring6.beans;public class Vip {private String email;private int age;public void setEmail(String email) {this.email = email;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Vip{" +"email='" + email + '\'' +", age=" + age +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="vipBean" class="com.xxx.spring6.bean.Vip"><!--带值注入 ,测试输出:Vip{email='111', age=20}--><!--<property name="email" value="111"/>--><!--<property name="age" value="20"/>--><!--方式一:不对属性注入,默认为null,测试输出:Vip{email='null', age=20}--><!--<property name="age" value="20"/> --><!--方式二:手动注入null,测试输出:Vip{email='null', age=20}--><!--<property name="email">--><!-- <null/>--><!--</property>--><!--<property name="age" value="20"/>--><!--空串的第一种方式,输出为:Vip{email='', age=0}--><!--<property name="email" value=""/>--><!--空串的第二种方式--><property name="email"><value/></property></bean>
</beans>
测试:
@Testpublic void testNull(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-null.xml");Vip vipBean = applicationContext.getBean("vipBean", Vip.class);System.out.println(vipBean);}
注入含有特殊符号:XML中有5个特殊字符,分别是:<、>、'、"、&
解决方法:
- 第一种:特殊符号使用转义字符代替;
- 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。
转义符号对照表:
使用<![CDATA[]]举例:
注意该方法只能用于value。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="mathBean" class="com.powernode.spring6.beans.Math"><property name="result"><!--只能使用value标签--><value><![CDATA[2 < 3]]></value></property></bean></beans>
p命名空间注入
p命名空间注入本质上还是set注入,可以用来简化配置文件,使用p命名空间注入的前提条件包括两个:
- 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p=“http://www.springframework.org/schema/p”
- p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
举例:
Dog
package com.xxx.spring6.bean;import java.util.Date;public class Dog {// 简单数据类型private String name;private int age;// 非简单类型private Date birth;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}public void setBirth(Date birth) {this.birth = birth;}@Overridepublic String toString() {return "Dog{" +"name='" + name + '\'' +", age=" + age +", birth=" + birth +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="dogBean" class="com.xxx.spring6.bean.Dog" p:name="发财" p:age="3" p:birth-ref="birthBean"/><bean id="birthBean" class="java.util.Date"/></beans>
测试:
@Testpublic void testSpringP(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-p.xml");Dog dog = applicationContext.getBean("dogBean", Dog.class);System.out.println(dog);}
c命名空间注入
c命名空间是简化构造方法注入的。使用c命名空间的两个前提条件:
- 第一:需要在xml配置文件头部添加信息:xmlns:c=“http://www.springframework.org/schema/c”
- 第二:需要提供构造方法。
举例:
MyTime
package com.xxx.spring6.bean;public class MyTime {private int year;private int month;private int day;public MyTime(int year, int month, int day) {this.year = year;this.month = month;this.day = day;}@Overridepublic String toString() {return "MyTime{" +"year=" + year +", month=" + month +", day=" + day +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:c="http://www.springframework.org/schema/c"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--<bean id="myTimeBean" class="com.xxx.spring6.bean.MyTime" c:_0="2023" c:_1="10" c:_2="25"/>--><bean id="myTimeBean" class="com.xxx.spring6.bean.MyTime" c:year="2023" c:month="10" c:day="25"/></beans>
测试:
@Testpublic void testSpringC(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-c.xml");MyTime myTime = applicationContext.getBean("myTimeBean", MyTime.class);System.out.println(myTime);}
注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。
util命名空间
使用util命名空间可以让配置复用。
使用util命名空间的前提是:在spring配置文件头部添加配置信息。
MyDataSource1:
package com.xxx.spring6.bean;import java.util.Properties;public class MyDataSource1 {private Properties properties;public void setProperties(Properties properties) {this.properties = properties;}@Overridepublic String toString() {return "MyDataSource1{" +"properties=" + properties +'}';}
}
MyDataSource2:
package com.xxx.spring6.bean;import java.util.Properties;public class MyDataSource2 {private Properties properties;public void setProperties(Properties properties) {this.properties = properties;}@Overridepublic String toString() {return "MyDataSource1{" +"properties=" + properties +'}';}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:util="http://www.springframework.org/schema/util"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"><util:properties id="prop"><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></util:properties><bean id="dataSource1" class="com.xxx.spring6.bean.MyDataSource1"><property name="properties" ref="prop"/></bean><bean id="dataSource2" class="com.xxx.spring6.bean.MyDataSource2"><property name="properties" ref="prop"/></bean></beans>
注意:修改了 xmlns:util和xsi:schemaLocation两处。
测试
@Testpublic void testSpringUtil(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-util.xml");MyDataSource1 dataSource1 = applicationContext.getBean("dataSource1", MyDataSource1.class);System.out.println(dataSource1);MyDataSource2 dataSource2 = applicationContext.getBean("dataSource2", MyDataSource2.class);System.out.println(dataSource2);}
输出:
MyDataSource1{properties={password=123456, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root}}
MyDataSource2{properties={password=123456, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root}}
基于XML的自动装配
Spring可以根据名字进行自动转配,也可以根据类型进行自动装配。自动装配是基于set注入的,因此要提供set函数。
根据名字自动装配举例:
package com.xxx.spring6.dao;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class OrderDao {private static final Logger logger = LoggerFactory.getLogger(OrderDao.class);public void insert(){System.out.println("订单正在生成……");}}
package com.xxx.spring6.service;import com.xxx.spring6.dao.OrderDao;public class OrderService {private OrderDao orderDao;// 通过set给orderDao赋值public void setOrderDao(OrderDao orderDao) {this.orderDao = orderDao;}//这是生成订单的业务方法public void generate(){orderDao.insert();}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--<bean id="orderDao" class="com.xxx.spring6.dao.OrderDao"/>--><!--<bean id="orderService" class="com.xxx.spring6.service.OrderService">--><!-- <property name="orderDao" ref="orderDao"/>--><!--</bean>--><bean id="orderDao" class="com.xxx.spring6.dao.OrderDao"/><!--id一般也叫作bean的名称--><!--注意:自动装配也是基于自动装配实现的--><!--根据名字进行自动-装配的时候,bean的id要和set方法后面的单词(首字母小写)一致--><bean id="orderService" class="com.xxx.spring6.service.OrderService" autowire="byName"/></beans>
测试:
@Testpublic void testAutoWire(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");OrderService orderService = applicationContext.getBean("orderService",OrderService.class);orderService.generate();}
关键:
- OrderService Bean 中需要添加autowire=“byName”,表示通过名称进行装配;
- OrderService类有orderDao属性,对应的set方法为setsetAaa方法,那么OrderDao Bean的id要命名为aaa。
根据类型自动装配:注意——根据类型匹配时,在有效的配置当中,该类型实例(Bean)只能有一个,否则会报错。
package com.xxx.spring6.dao;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class UserDao {private static final Logger logger = LoggerFactory.getLogger(UserDao.class);public void insert(){//System.out.println("数据库正在保存用户信息……");// 使用kog4j2日志框架logger.info("数据库正在保存用户信息……");}}
package com.xxx.spring6.dao;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class VipDao {private static final Logger logger = LoggerFactory.getLogger(VipDao.class);public void insert(){//System.out.println("数据库正在保存用户信息……");// 使用kog4j2日志框架logger.info("数据库正在保存VIP用户信息……");}}
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import com.xxx.spring6.dao.VipDao;public class CustomerService {private UserDao userDao;private VipDao vipDao;//public CustomerService(UserDao userDao, VipDao vipDao) {// this.userDao = userDao;// this.vipDao = vipDao;//}public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void setVipDao(VipDao vipDao) {this.vipDao = vipDao;}public void save(){userDao.insert();vipDao.insert();}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--<bean id="orderDao" class="com.xxx.spring6.dao.OrderDao"/>--><!--<bean id="orderService" class="com.xxx.spring6.service.OrderService">--><!-- <property name="orderDao" ref="orderDao"/>--><!--</bean>--><bean class="com.xxx.spring6.dao.VipDao"></bean><bean class="com.xxx.spring6.dao.UserDao"></bean><bean id="cs" class="com.xxx.spring6.service.CustomerService" autowire="byType"></bean></beans>
测试:
@Testpublic void testAutoWire(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");CustomerService cs = applicationContext.getBean("cs",CustomerService.class);cs.save();}
spring引入外部属性配置文件
(这里的代码直接复制的教程)
第一步:写一个数据源类,提供相关属性。
package com.xxx.spring6.bean;public class MyDataSource {@Overridepublic String toString() {return "MyDataSource{" +"driver='" + driver + '\'' +", url='" + url + '\'' +", username='" + username + '\'' +", password='" + password + '\'' +'}';}private String driver;private String url;private String username;private String password;public void setDriver(String driver) {this.driver = driver;}public void setUrl(String url) {this.url = url;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}}
第二步:在类路径下新建jdbc.properties文件,并配置信息。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
第三步:在spring配置文件中引入context命名空间。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="jdbc.properties"/><bean id="dataSource" class="com.xxx.spring6.bean.MyDataSource"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></bean>
</beans>
测试程序:
@Test
public void testProperties(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-properties.xml");MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);System.out.println(dataSource);
}
输出:
MyDataSource{driver='com.mysql.cj.jdbc.Driver', url='jdbc:mysql://localhost:3306/spring', username='zxy', password='root123'}
Bean的作用域
Spring默认情况下管理Bean:
- 默认情况下Bean是单例的;
- 在Spring上下文初始化的时候实例化;
- 在每一次调用getBean()方法的时候,都返回那个单例(singleton)的对象。
设置Bean的作用域:
<bean id="userDaoBean" class="com.xxx.spring6.dao.UserDao" scope="prototype"/>
scope表示当前Bean的作用域,singleton(默认值)表示单例,prototype表示多例(原型)。
如果将Bean的scope属性设置为prototype,则spring初始化上下文的时候,不会实例化该Bean对象,每一次调用getBean()方法的时候,实例化该Bean对象。
如果当前项目是一个Web项目(比如在pom.xml中引入spring-web的时候),scope还可以设置其他值,scope的取值一共有8种:
- singleton:默认的,单例。
- prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
- request:一个请求对应一个Bean。仅限于在WEB应用中使用。
- session:一个会话对应一个Bean。仅限于在WEB应用中使用。
- global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
- application:一个应用对应一个Bean。仅限于在WEB应用中使用。
- websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
- 自定义scope:很少使用。
GoF之工厂模式
设计模式:一种可以被重复利用的解决方案。
《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
GoF23种设计模式可分为三大类:
- 创建型(5个):解决对象创建问题。
- 单例模式
- 工厂方法模式
- 抽象工厂模式
- 建造者模式
- 原型模式
- 结构型(7个):一些类或对象组合在一起的经典结构。
- 代理模式
- 装饰模式
- 适配器模式
- 组合模式
- 享元模式
- 外观模式
- 桥接模式
- 行为型(11个):解决类或对象之间的交互问题。
- 策略模式
- 模板方法模式
- 责任链模式
- 观察者模式
- 迭代子模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
工厂模式的三种形态
工厂模式通常有三种形态:
- 简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
- 工厂方法模式(Factory Method):是23种设计模式之一。
- 抽象工厂模式(Abstract Factory):是23种设计模式之一。
简单工厂模式
简单工厂模式的角色:
- 抽象产品 角色
- 具体产品 角色
- 工厂类 角色(工厂负责生产的方法是静态方法)
简单工厂模式优缺点:
- 优点:
- 客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
- 缺点:
- 工厂类负责所有产品的创造逻辑,形成一个无所不知的全能类,或者把它叫做上帝类。显然工厂类非常关键,不能出问题,一旦出问题,整个系统瘫痪。
- 不符合OCP开闭原则,在进行系统扩展时(例如新增具体类),需要修改工厂类。Spring中的BeanFactory就使用了简单工厂模式。
工厂方法模式
工厂方法模式的角色包括:
- 抽象产品角色
- 具体产品角色
- 抽象工厂角色
- 具体工厂角色
工厂方法模式的优缺点:
- 优点
- 扩展一个具体的产品的时候,我们只需要添加两个类,一个类是具体产品类,另一个是具体工厂类,都是添加操作,不需要改原有的代码,因此复合OCP原则。
- 一个调用者想创建一个具体产品,只要知道其名称就可以了。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
- 缺点
- 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
抽象工厂模式
工厂方法模式的角色包括:
- 抽象产品角色
- 具体产品角色
- 抽象工厂角色
- 具体工厂角色
类之间的关系:
抽象工厂模式的优缺点:
- 优点
- 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
- 缺点
- 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
Bean的获取方式
Spring有4种常用的获取化Bean的方式:
- 通过构造方法获取
- 通过简单工厂模式获取
- 通过factory-bea获取
- 通过FactoryBean接口获取
通过构造方法获取
其实就是构造方法注入的方式,举例:
package com.xxx.spring6.bean;public class SpringBean {public SpringBean() {System.out.println("Bean 的五参数构造方法执行");}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--Spring实例化Bean的第一种:在spring配置文件中直接配置类全路径,spring会自动调用该类的无参数构造构造方法来实例化Bean--><bean id="sb" class="com.xxx.spring6.bean.SpringBean"/></beans>
@Testpublic void testIntantiation2() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Star sf = applicationContext.getBean("sf", Star.class);System.out.println(sf);}
简单工厂模式获取
在构造方法获取的基础上,添加了一个工厂类,该工厂有一个静态的生产方法get用于获取实例,通过在Spring配置文件中设置获取Bean实例的方式,举例如下:
package com.xxx.spring6.bean;public class Star {public Star() {System.out.println("正在创建star对象……");}
}
package com.xxx.spring6.bean;public class StarFactory {static public Star get(){return new Star();}
}
配置文件直接在上面的基础上添加就可以了:
<!--Spring提供的实例化方式,第二种:通过简单工厂模式。我们需要再Spring配置文件中告诉Spring框架,调哪个方法获取Bean--><!--factory-method 属性是指的是工厂类当中的静态方法。也就是告诉Spring框架,调用这个方法可以获取Bean--><bean id="star" class="com.xxx.spring6.bean.StarFactory" factory-method="get"/>
@Testpublic void testIntantiation2() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Star st = applicationContext.getBean("star", Star.class);System.out.println(sf);}
工厂方法模式获取
该方式与简单工厂模式的差别在于——简单工厂模式中工厂使用的静态方法获取实例对象;而工厂方法模式中采用的是实例方法获取实例对象,因此要调用该工厂的生产方法get,就需要实例化工厂类。举例如下:
package com.xxx.spring6.bean;/*** ClassName: Gun 工厂方法模式当中的具体产品角色*/public class Gun {public Gun() {System.out.println("正在创建Gun产品……");}
}
package com.xxx.spring6.bean;/*** ClassName: GunFactory 工厂方法模式当中的具体工厂角色*/public class GunFactory {// 工厂方法模式中的具体工厂角色的方法是:实例方法。public Gun get(){// 实际上new这个对象还是我们程序员自己new的return new Gun();}}
配置文件直接在上面的基础上添加就可以了:
<!--Spring提供的实例化方式,第三种:通过工厂方法模式。通过factory-bean属性+factory-method属性来共同完成--><!--告诉Spring框架,调用哪个对象的那个方法来获取Bean--><bean id="gunFactory" class="com.xxx.spring6.bean.GunFactory"/><!--factory-bean告诉Spring调用哪个对象,factory-method告诉Spring调用该对象的哪个方法--><bean id="gun" factory-bean="gunFactory" factory-method="get"/>
@Testpublic void testIntantiation3() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Gun gun = applicationContext.getBean("gun", Gun.class);System.out.println(gun);}
FactoryBean接口实例化
工厂方法模式中,factory-bean是我们自定义的,factory-method也是我们自己定义的。
在Spring中,当你编写的类直接实现FactoryBean接口之后,factory-bean不需要指定了,factory-method也不需要指定了。
factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法。
举例:
package com.xxx.spring6.bean;public class Person { // PersonFactoryBean是一个普通Bean。public Person() {System.out.println("Person的无参构造方法……");}
}
package com.xxx.spring6.bean;import org.springframework.beans.factory.FactoryBean;public class PersonFactoryBean implements FactoryBean<Person>{ // PersonFactoryBean是一个工厂Bean,可以通过工厂Bean获取普通Bean。//这个方法在接口中有默认实现,默认返回true,表示单例的,如果想要多例,则返回false@Overridepublic boolean isSingleton() {return FactoryBean.super.isSingleton();}@Overridepublic Person getObject() throws Exception {//最终这个Bean的创建还是程序员自己new的return new Person();}@Overridepublic Class<?> getObjectType() {return null;}
}
配置文件直接在上面的基础上添加就可以了:
<!--Spring提供的实例化方式,第四种:通过FactoryBean接口来实现--><!--这种方式实际上就是第三种方式的简化--><!--由于编写的类实现了FactoryBean接口,所以这个类是一个特殊的类,不需要手动指定factory-bean和factory-method--><!--通过这个特殊的工厂Bean来返回一个普通的Bean Person对象--><bean id="person" class="com.xxx.spring6.bean.PersonFactoryBean"/>
@Testpublic void testIntantiation4() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Person person = applicationContext.getBean("person", Person.class);System.out.println(person);}
BeanFactory和FactoryBean的区别
- BeanFactory,被翻译为“Bean工厂”,是Spring IoC容器的顶级对象,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。BeanFactory是工厂。
- FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。在Spring中,Bean可以分为两类:
- 普通Bean
- 工厂Bean(工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)
Bean的生命周期
Spring就是一个管理Bean对象的工厂,它负责Bean对象的创建,对象的销毁等。
生命周期——对象从创建开始到最终销毁的整个过程,其本质是在特定的时间点上调用特定的方法。
Bean生命周期之5步
Bean生命周期可以粗略的划分为五大步:
- 第一步:实例化Bean——调用无参数构造方法;
- 第二步:Bean属性赋值——调用set方法;
- 第三步:初始化Bean——调用Bean的init方法,这个方法要自己写、自己配(方法名随意);
- 第四步:使用Bean;
- 第五步:销毁Bean——调用Bean的destroy方法,这个方法也要自己写、自己配(方法名随意)。
Bean生命周期的管理,可以参考Spring的源码:AbstractAutowireCapableBeanFactory类的doCreateBean()方法。
测试程序:
package com.xxx.spring6.bean;public class User {private String name;public User() {System.out.println("1.实例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean属性赋值");}public void initBean(){System.out.println("3.初始化Bean");}public void destroyBean(){System.out.println("5.销毁Bean");}}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--init-method属性指定初始化方法。destroy-method属性指定销毁方法。--><bean id="userBean" class="com.xxx.spring6.bean.User" init-method="initBean" destroy-method="destroyBean"><property name="name" value="zhangsan"/></bean></beans>
@Testpublic void testLifecycle() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println("4.使用Bean");// 只有正常关闭spring容器才会执行销毁方法ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;context.close();}
执行结果:
Bean 的五参数构造方法执行
正在创建star对象……
正在创建Gun产品……
Person的无参构造方法……
com.xxx.spring6.bean.Person@14fc1f0
注意:
- 只有正常关闭spring容器,bean的销毁方法才会被调用。
- ClassPathXmlApplicationContext类才有close()方法。
- 配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。
Bean生命周期之7步
若我们想在Bean初始化化前和初始化后添加代码,可以加入“Bean后处理器”。这样Bean的生命周期就有了7步:
加入“Bean后处理器”:
1)编写一个类实现BeanPostProcessor类,并且重写before和after方法:
package com.xxx.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;public class LogBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {System.out.println("执行Bean后处理器的before方法");return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);}// 方法有两个参数:// 第一个参数:刚创建的bean对象;// 第二个参数:bean的名字@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("执行Bean后处理器的after方法");return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);}
}
2)在spring.xml文件中配置“Bean后处理器”:
<!--配置Bean后处理器--><!--注意:这个Bean后处理器将作用于整个配置文件中所有的bean--><bean class="com.xxx.spring6.bean.LogBeanPostProcessor"/>
一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。
Bean生命周期之10步
添加的三步分别是:
- 处理器before的方法之前
- 检查Bean是否实现了Aware相关的接口,如果实现了接口则调用这些接口中的方法,通过这些方法可以传递我们需要的数据。
- 处理器before的方法之后
- 检查是否实现了InitializingBean接口,如果实现了,则调用接口中的方法。
- 使用Bean之后
添加的这三个点为的特点:都是检查这个Bean是否实现了某些特定的接口,如果实现了这些接口,则Spring容器会调用这个接口中的方法。
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
- 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
- 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
- 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
测试以上10步,可以让User类实现5个接口,并实现所有方法:
- BeanNameAware
- BeanClassLoaderAware
- BeanFactoryAware
- InitializingBean
- DisposableBean
测试代码如下:
package com.xxx.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {private String name;public User() {System.out.println("1.实例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean属性赋值");}public void initBean(){System.out.println("6.初始化Bean");}public void destroyBean(){System.out.println("10.销毁Bean");}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {System.out.println("3.类加载器:" + classLoader);}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {System.out.println("3.Bean工厂:" + beanFactory);}@Overridepublic void setBeanName(String name) {System.out.println("3.bean名字:" + name);}@Overridepublic void destroy() throws Exception {System.out.println("9.DisposableBean destroy");}//来自InitializingBean@Overridepublic void afterPropertiesSet() throws Exception {System.out.println("5.afterPropertiesSet执行");}
}
结果:
1.实例化Bean
2.Bean属性赋值
3.bean名字:userBean
3.类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
3.Bean工厂:org.springframework.beans.factory.support.DefaultListableBeanFactory@78a773fd: defining beans [com.xxx.spring6.bean.LogBeanPostProcessor#0,userBean]; root of factory hierarchy
执行Bean后处理器的before方法
5.afterPropertiesSet执行
6.初始化Bean
执行Bean后处理器的after方法
4.使用Bean
9.DisposableBean destroy
10.销毁Bean
注意:Spring对不同作用域的Bean有不同管理方式——
- 对于singleton作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;
- 对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
让Spring管理程序员new的对象
@Testpublic void testBeanRegister() {// 自己new的对象User user = new User();System.out.println(user);//创建 默认可列表BeanFactory 对象DefaultListableBeanFactory factory = new DefaultListableBeanFactory();// 注册factory.registerSingleton("userBean",user);// 从Spring容器中获取BeanUser userBean = factory.getBean("userBean",User.class);System.out.println(userBean);}
测试结果:
1.实例化Bean
com.xxx.spring6.bean.User@1ae369b7
com.xxx.spring6.bean.User@1ae369b7
Bean的循环依赖问题
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
测试举例:
package com.xxx.spring6.bean;public class Husband {private String name;private Wife wife;public void setName(String name) {this.name = name;}public void setWife(Wife wife) {this.wife = wife;}public String getName() {return name;}@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}
}
package com.xxx.spring6.bean;public class Wife {private String name;private Husband husband;public void setHusband(Husband husband) {this.husband = husband;}public void setName(String name) {this.name = name;}public String getName() {return name;}@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}
<bean id="husbandBean" class="com.xxx.spring6.bean.Husband" scope="singleton"><property name="name" value="张三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.xxx.spring6.bean.Wife" scope="singleton"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
测试:
@Testpublic void testCD(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Husband husband = applicationContext.getBean("husbandBean", Husband.class);System.out.println(husband);Wife wife = applicationContext.getBean("wifeBean", Wife.class);System.out.println(wife);}
输出
Husband{name='张三', wife=小花}
Wife{name='小花', husband=张三}
在singleton+setter模式下,循环依赖不会出现问题的原因:
在这种模式下,Spring对Bean的管理主要分为清晰的两个阶段:
- 第一个阶段:在Spring容器加载的时候,实例Bean,只要其中任意一个Bean实例化后,马上进行曝光【曝光不等于属性赋值就曝光】
- 第二个阶段:Bean“曝光”之后,再进行属性的赋值(调用set方法)。
核心解决方案就是——实例化对象和对象的属性赋值分为两个阶段来完成的。注意:只有在scope是singleton的情况下,Bean才会采取提前“曝光”的措施。(因为singleton的模式下,可以保证该类型的对象在整个Spring容器中只有一个。)
如果是prototype + setter模式下出现循环依赖,则代码会抛出异常:.BeanCurrentlyInCreationException,表示当前Bean正处于创建中异常,如图所示:
如果其中一个是singleton+setter,其他是prototype + setter模式,不会出现上述异常。
singleton+构造注入产生的循环依赖:
package com.xxx.spring6.bean2;import com.xxx.spring6.bean2.Wife;public class Husband {private String name;private Wife wife;public Husband(String name, Wife wife) {this.name = name;this.wife = wife;}public String getName() {return name;}@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}
}
package com.xxx.spring6.bean2;import com.xxx.spring6.bean.Husband;public class Wife {private String name;private Husband husband;public Wife(String name, Husband husband) {this.name = name;this.husband = husband;}public String getName() {return name;}@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="h" scope="singleton" class="com.xxx.spring6.bean2.Husband"><constructor-arg index="0" value="张三"/><constructor-arg index="1" ref="w"/></bean><bean id="w" scope="singleton" class="com.xxx.spring6.bean2.Wife"><constructor-arg index="0" value="小花"/><constructor-arg index="1" ref="h"/></bean>
</beans>
测试:
@Testpublic void testCD2(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");Husband husband = applicationContext.getBean("h", Husband.class);System.out.println(husband);Wife wife = applicationContext.getBean("w", Wife.class);System.out.println(wife);}
依旧会抛出BeanCurrentlyInCreationException异常,**原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。**因此基于构造方式下产生的循环依赖也是无法解决的。
Spring解决循环依赖(set + singleton模式)的机理: ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
反射机制
根据方法的调用过程,我们知道调用一个方法,一般涉及到4个要素:
- 调用哪个对象的
- 调用哪个方法
- 传什么参数
- 返回什么值
通过反射机制调用方法,我们也要按照上诉步骤来:
- 获取这个类Class,并创建调用对象:
- Class clazz = Class.forName(“com.powernode.reflect.SystemService”); // 获得类
- Object obj = clazz.newInstance();
- 获取方法:
- Method loginMethod = clazz.getDeclaredMethod(“login”, String.class, String.class); //获取方法,第一个参数是方法名,后面的参数是该方法的形参类型
- 若需要获取的方法的形参类型未知,但我们知道该方法针对该类中的哪个属性使用,我们可以通过以下方式获得该属性的类型:Field field = clazz.getDeclaredField(属性名);
- 调用方法:
- 有返回值:Object retValue = loginMethod.invoke(obj, “admin”, “admin123”);
- 没有返回值:oginMethod.invoke(obj, “admin”, “admin123”);
Spring中就是通过解析XML配置文件,然后利用反射机制创建Bean实例的。
Spring IoC注解开发
自定义注解与使用
自定义注解:
/* 标注注解的注解,叫做元注解。@Target注解用来修饰@Component可以出现的位置。@Target(value = {ElementType.TYPE, ElementType.FIELD})表示@Component注解可以出现在类(接口)上、属性上。@Target(value = {ElementType.TYPE})表示@Component注解只能出现在类(接口)上小技巧:使用某个注解的时候,如果注解的属性名是value的话,value可以省略,比如:@Target({ElementType.TYPE})使用某个注解的时候,如果注解的属性值是数组,并且数组中只有一个元素,大括号可以省略,比如:@Target(ElementType.TYPE)@Retention 也是一个元注解。用来标注@Component注解最终保留在class文件当中,并且可以被反射机制读取。注解的单独使用没有意义,正确的用法是配合反射机制使用:通过反射机制可以读取到某一个类或属性或方法上的注解里面提供的属性值
* */@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) // 唯有RUNTIME阶段才能被反射读取到
public @interface Component {// 定义注解的属性// String是属性类型// value是属性名String value();
}
以上是自定义了一个注解:Component
该注解上面修饰的注解包括:Target注解和Retention注解,这两个注解被称为元注解(用在注解的注解)。
Target注解用来设置Component注解可以出现的位置,以上代表表示Component注解只能用在类和接口上。
Retention注解用来设置Component注解的保持性策略,以上代表Component注解可以被反射机制读取。
注解的使用:
package com.xxx.bean;import com.powernode.xxx.Component;// @Component(value = "userBean")
@Component("userBean") // 省略value
public class User {
}
用法简单,语法格式:@注解类型名(属性名=属性值, 属性名=属性值, 属性名=属性值…)
反射解析注解:
package com.xxx.test;import com.xxx.annotation.Component;import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;public class Test {public static void main(String[] args) throws Exception {// 存放Bean的Map集合。key存储beanId。value存储Bean。Map<String,Object> beanMap = new HashMap<>();// 获取包的完整路径String packageName = "com.xxx.bean";String path = packageName.replaceAll("\\.", "/");URL url = ClassLoader.getSystemClassLoader().getResource(path);File file = new File(url.getPath());File[] files = file.listFiles();Arrays.stream(files).forEach(f -> {String className = packageName + "." + f.getName().split("\\.")[0];try {Class<?> clazz = Class.forName(className);// 判断该类是否有Component注解if (clazz.isAnnotationPresent(Component.class)) {// 解析注解Component component = clazz.getAnnotation(Component.class);String beanId = component.value();Object bean = clazz.newInstance();beanMap.put(beanId, bean);}} catch (Exception e) {e.printStackTrace();}});System.out.println(beanMap);}
}
声明Bean的注解
负责声明Bean的注解,常见的包括四个:
- @Component
- @Controller:表示层建议使用Controller注解
- @Service:业务层建议使用Service注解
- @Repository:持久层建议使用Repository注解
以上的建议是为了增强程序的可读性。
源码如下:
@Component注解:
package com.powernode.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {String value();
}
@Controller注解:
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {@AliasFor(annotation = Component.class)String value() default "";
}
@Service注解:
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {@AliasFor(annotation = Component.class)String value() default "";
}
@Repository注解:
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {@AliasFor(annotation = Component.class)String value() default "";
}
通过源码可以看到,@Controller、@Service、@Repository这三个注解都是@Component注解的别名( @AliasFor这个表示别名)。也就是说:这四个注解的功能都一样。用哪个都可以。他们都是只有一个value属性。value属性用来指定bean的id,也就是bean的名字。
Spring 注解的使用
使用步骤:
- 加入aop的依赖
- 在配置文件中添加context命名空间
- 在配置文件中指定扫描的包
- 在Bean类上使用注解
第一步:加入aop的依赖
可以看到当加入spring-context依赖之后,会关联加入aop的依赖:
第二步:在配置文件中添加context命名空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"></beans>
第三步:在配置文件中指定要扫描的包
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--给Spring框架指定要扫描哪些包中的类--><context:component-scan base-package="com.xxx.spring6.bean"/>
</beans>
第四步:在Bean类上使用注解
package com.xxx.spring6.bean;import org.springframework.stereotype.Component;@Component(value = "userBean")
public class User {
}
测试:
@Testpublic void testBean(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println(userBean);}
小细节:查看@Component源码可以发现,value具有默认值,也就是说,我们在给类添加该注解时,如果没有指定value值,那么Spring会自动给该Bean取值,并且默认名字的规律是:该Bean类名首字母小写即可,例如BankDao的bean的名字为bankDao。
若要扫描的包有多个,有两种解决方案:
- 在配置文件中指定多个包,用逗号隔开。
- 指定多个包的共同父包(扫描范围变大,这会牺牲一部分效率)。
第一种方式:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.xxx.spring6.bean,com.xxx.spring6.bean2"/>
</beans>
第二种方式:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.xxx.spring6"/>
</beans>
选择性实例化Bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?
架设几个类都定义在同一个java源文件中:
package com.xxx.spring6.bean3;import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;@Component
public class A {public A() {System.out.println("A的无参数构造方法执行");}
}@Controller
class B {public B() {System.out.println("B的无参数构造方法执行");}
}@Service
class C {public C() {System.out.println("C的无参数构造方法执行");}
}@Repository
class D {public D() {System.out.println("D的无参数构造方法执行");}
}@Controller
class E {public E() {System.out.println("E的无参数构造方法执行");}
}@Controller
class F {public F() {System.out.println("F的无参数构造方法执行");}
}
如果只想实例化bean3包下的Controller,配置文件为:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--方式一:--><context:component-scan base-package="com.xxx.spring6.bean3" use-default-filters="false"><!--只有@Controller被包含进来,让该注解生效--><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!--方式二:--><context:component-scan base-package="com.powernode.spring6.bean3"><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan>
</beans>
- 当use-default-filters="false"时,表示com.xxx.spring6.bean3包下所有带声明Bean的注解全部失效,即Component、Controller、Service、Repository全部失效。
- 当use-default-filters="true"时(默认值),表示com.xxx.spring6.bean3包下所有带声明Bean的注解全部生效。
负责注入的注解
通过注解给Bean的属性赋值,需要用到这些注解:
- @Value
- @Autowired
- @Qualifier
- @Resource
@Value的使用
当属性的类型时简单类型时,可以使用@Value注解进行注入:
package com.xxx.spring6.bean4;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class User {@Value(value = "zhangsan")private String name;@Value("20")private int age;@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.xxx.spring6.bean4"/>
</beans>
@Test
public void testValue(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");Object user = applicationContext.getBean("user");System.out.println(user);
}
注意: 使用@Value注解注入属性值的时候,可以用在属性上,并且不提供set方法。
将@Value用在setter方法上:
package com.xxx.spring6.bean4;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class User {private String name;private int age;@Value("李四")public void setName(String name) {this.name = name;}@Value("30")public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
通过构造方法注入
package com.powernode.xxx.bean4;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class User {private String name;private int age;public User(@Value("隔壁老王") String name, @Value("33") int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
通过以上代码:@Value注解可以出现在属性上、setter方法上、以及构造方法的形参上。
@Autowired与@Qualifier
@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者自动装配。单独使用@Autowired注解,默认根据类型装配。【默认是byType】如果想要根据名字进行装配,则要一起使用@Autowired与@Qualifier。
@Autowired源码:
package org.springframework.beans.factory.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {boolean required() default true;
}
通过源码,可以知道:
- @Autowired 的使用范围:构造方法、方法、形参、属性、注解;
- 该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
属性上使用@Autowired :
package com.xxx.spring6.dao;public interface UserDao {void insert();
}
package com.xxx.spring6.dao;import org.springframework.stereotype.Repository;@Repository //纳入bean管理
public class UserDaoForMySQL implements UserDao{@Overridepublic void insert() {System.out.println("正在向mysql数据库插入User数据");}
}
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service // 纳入bean管理
public class UserService {@Autowired // 在属性上注入private UserDao userDao;// 没有提供构造方法和setter方法。public void save(){userDao.insert();}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.xxx.spring6.dao,com.xxx.spring6.service"/>
</beans>
@Test
public void testAutowired(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");UserService userService = applicationContext.getBean("userService", UserService.class);userService.save();
}
setter方法 测试@Autowired注解:
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserService {private UserDao userDao;@Autowiredpublic void setUserDao(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
构造方法 测试@Autowired注解:
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserService {private UserDao userDao;// 方式一//@Autowired//public UserService(UserDao userDao) {// this.userDao = userDao;//}// 方式二public UserService(@Autowired UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
当构造方法只有一个,同时该构造方法只有一个形参且能匹配,@Autowired注解可以省略(但最好别省):
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import org.springframework.stereotype.Service;@Service
public class UserService {private UserDao userDao;public UserService(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
注意:如果有多个构造方法,@Autowired肯定是不能省略的。
结合上述程序,若UserDao接口还有另一个实现类:
package com.xxx.spring6.dao;import org.springframework.stereotype.Repository;@Repository //纳入bean管理
public class UserDaoForOracle implements UserDao{@Overridepublic void insert() {System.out.println("正在向Oracle数据库插入User数据");}
}
此时,使用@Autowired注解会报错,因为此时Spring不能识别要装配UserDao下的哪一个实现类了。为了解决这个问题,我们可以通过byName 根据名称进行装配。
@Autowired注解和@Qualifier注解联合起来才可以根据名称进行装配,在@Qualifier注解中指定Bean名称。
package com.xxx.spring6.dao;import org.springframework.stereotype.Repository;@Repository // 这里没有给bean起名,默认名字是:userDaoForOracle
public class UserDaoForOracle implements UserDao{@Overridepublic void insert() {System.out.println("正在向Oracle数据库插入User数据");}
}
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;@Service
public class UserService {private UserDao userDao;@Autowired@Qualifier("userDaoForOracle") // 这个是bean的名字。public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
总结:
- @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
- 当带参数的构造方法只有一个,@Autowired注解可以省略。
- @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
@Resource(官方建议使用)
@Resource注解也可以完成非简单类型注入。那它和@Autowired注解有什么区别?
- @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
- @Autowired注解是Spring框架自己的。
- @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
- @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
- @Resource注解用在属性上、setter方法上。
- @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。
@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。】
如果你是Spring6+ 版本请使用这个依赖:
<dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version>
</dependency>
注意:如果你用Spring6,要知道Spring6不再支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE贡献给Apache了,Apache把JavaEE的名字改成JakartaEE了,大家之前所接触的所有的 javax. 包名统一修改为 jakarta.包名了。
如果你是Spring5- 版本请使用这个依赖:
<dependency><groupId>javax.annotation</groupId><artifactId>javax.annotation-api</artifactId><version>1.3.2</version>
</dependency>
@Resource注解的源码:
package jakarta.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Resources.class)
public @interface Resource {String name() default "";String lookup() default "";Class<?> type() default Object.class;AuthenticationType authenticationType() default Resource.AuthenticationType.CONTAINER;boolean shareable() default true;String mappedName() default "";String description() default "";public static enum AuthenticationType {CONTAINER,APPLICATION;private AuthenticationType() {}}
}
通过源码可得:
- @Resource可以用在类、属性、方法上;
- name属性用来接收bean的名称。未指定name时,默认使用"属性名"作为name,然后Spring会根据"属性名"去查找有没有一个声明Bean时取名跟"属性名"同名的那样一个类,如果找到了就装配上,如果没找到,再根据类型byType装配。但是如果这时出现了多个和属性同类型的类,就会出错,无法自动装配。
使用举例:
package com.xxx.spring6.dao;import org.springframework.stereotype.Repository;@Repository("xyz")
public class UserDaoForOracle implements UserDao{@Overridepublic void insert() {System.out.println("正在向Oracle数据库插入User数据");}
}
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;@Service
public class UserService {@Resource(name = "xyz")private UserDao userDao;public void save(){userDao.insert();}
}
把UserDaoForOracle的名字xyz修改为userDao,让这个Bean的名字和UserService类中的UserDao属性名一致:
package com.xxx.spring6.dao;import org.springframework.stereotype.Repository;@Repository("userDao")
public class UserDaoForOracle implements UserDao{@Overridepublic void insert() {System.out.println("正在向Oracle数据库插入User数据");}
}
UserService类中Resource注解并没有指定name:
package com.xxx.spring6.service;import com.xxx.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;@Service
public class UserService {@Resourceprivate UserDao userDao;public void save(){userDao.insert();}
}
全注解式开发
所谓的全注解开发就是不再使用spring配置文件了。写一个配置类来代替配置文件。
配置类代替Spring配置文件:
package com.xxx.spring6.config;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;@Configuration
@ComponentScan({"com.xxx.spring6.dao", "com.xxx.spring6.service"})
public class Spring6Configuration {
}
编写测试程序:不再new ClassPathXmlApplicationContext()对象了。
@Test
public void testNoXml(){ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);UserService userService = applicationContext.getBean("userService", UserService.class);userService.save();
}
GoF之代理模式
代理模式是GoF23种设计模式之一,属于结构型设计模式。
业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
代理模式中的角色:
- 代理类(代理主题)
- 目标类(真实主题)
- 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。
代理模式在代码实现上,包括两种形式: - 静态代理
- 动态代理
静态代理举例:
OrderService接口:
package com.xxx.mall.service;public interface OrderService {/*** 生成订单*/void generate();/*** 查看订单详情*/void detail();/*** 修改订单*/void modify();
}
OrderService接口的实现类:
package com.xxx.mall.service.impl;import com.xxx.mall.service.OrderService;public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已生成");}@Overridepublic void detail() {try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");}@Overridepublic void modify() {try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");}
}
客户端:
package com.xxx.mall;import com.xxx.mall.service.OrderService;
import com.xxx.mall.service.OrderServiceProxy;
import com.xxx.mall.service.impl.OrderServiceImpl;public class Client {public static void main(String[] args) {OrderService order = new OrderServiceImpl();order.generate();order.modify();order.detail();}
}
现有需求——获取每次操作的运行时间:
添加静态代理——为OrderService接口提供一个代理类:
package com.xxx.mall.service;public class OrderServiceProxy implements OrderService{ // 代理对象// 目标对象// 将目标对象作为代理对象的一个属性,这种关系叫做关联关系,必继承关系的耦合度低。private OrderService orderService;// 通过构造方法将目标对象传递给代理对象public OrderServiceProxy(OrderService orderService) {this.orderService = orderService;}@Overridepublic void generate() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.generate();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.detail();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.modify();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}
}
客户端:
package com.xxx.mall;import com.xxx.mall.service.OrderService;
import com.xxx.mall.service.OrderServiceProxy;
import com.xxx.mall.service.impl.OrderServiceImpl;public class Client {public static void main(String[] args) {// 创建目标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService proxy = new OrderServiceProxy(target);// 调用代理对象的代理方法proxy.generate();proxy.modify();proxy.detail();}
}
以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。
缺点:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。解决方式——动态代理,在内存中动态的为我们生成代理类的字节码。代码只需要写一次,以后得代理可以直接复用该代码。
动态代理:在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量,解决代码复用的问题。
在内存当中动态生成类的技术常见的包括:
- JDK动态代理技术(java.lang.reflect.Proxy):只能代理接口。
- CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
- Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
动态代理举例:
OrderService接口:
package com.xxx.mall.service;public interface OrderService {/*** 生成订单*/void generate();/*** 查看订单详情*/void detail();/*** 修改订单*/void modify();
}
OrderService接口的实现类:
package com.xxx.mall.service.impl;import com.xxx.mall.service.OrderService;public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已生成");}@Overridepublic void detail() {try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");}@Overridepublic void modify() {try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");}
}
客户端:
package com.xxx.mall;import com.xxx.mall.service.OrderService;
import com.xxx.mall.service.impl.OrderServiceImpl;import java.lang.reflect.Proxy;public class Client {public static void main(String[] args) {// 第一步:创建目标对象OrderService target = new OrderServiceImpl();// 第二步:创建代理对象OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);// 第三步:调用代理对象的代理方法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}
OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);
这行代码做了两件事:
- 第一件事:在内存中动态生成了代理类的字节码
- 第二件事:通过内存中生成的字节码,创建代理对象
newProxyInstance()方法有三个参数:
- ClassLoader loader:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的,所以要指定使用哪个类加载器加载。并且,JDK要求目标类的类加载器必须和代理类的类加载器使用同一个。
- Class<?>[] interfaces:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
- InvocationHandler h:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。在调用处理器接口中写的就是:增强代码。
java.lang.reflect.InvocationHandler接口的实现类:
package com.xxx.mall.service;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class TimerInvocationHandler implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return null;}
}
强行要求实现InvocationHandler接口的原因:因为JDK在底层调用invoke()方法的程序已经提前写好了,所以必须实现这个接口。注意:invoke方法不是程序员负责调用的,而是JDK负责调用的。
invoke什么时候被调用:当代理对象调用代理方法的时候,注册在InvocationHandler处理器当中的invoke被调用。
InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:
- 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
- 第二个参数:Method method。目标方法。
- 第三个参数:Object[] args。目标方法调用时要传的参数。
完善——给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:
package com.xxx.mall.service;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class TimerInvocationHandler implements InvocationHandler {// 目标对象private Object target;// 通过构造方法来传目标对象public TimerInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 目标执行之前增强。long begin = System.currentTimeMillis();// 调用目标对象上的目标方法// 方法四要素:哪个对象,哪个方法,传什么参数,返回什么值Oject retValue = method.invoke(target,args);// 目标执行之后增强。long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");System.out.println("增强2");// 代理对象需要返回的结果return retValue;}
}
完善——Client程序:
package com.xxx.mall;import com.xxx.mall.service.OrderService;
import com.xxx.mall.service.TimerInvocationHandler;
import com.xxx.mall.service.impl.OrderServiceImpl;import java.lang.reflect.Proxy;public class Client {public static void main(String[] args) {// 创建目标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new TimerInvocationHandler(target));// 调用代理对象的代理方法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}
为了简化代码,实现一个工具类:
package com.xxx.mall.util;import com.xxx.mall.service.TimerInvocationHandler;import java.lang.reflect.Proxy;public class ProxyUtil {public static Object newProxyInstance(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimerInvocationHandler(target));}
}
客户端代码:
package com.xxx.mall;import com.xxx.mall.service.OrderService;
import com.xxx.mall.service.TimerInvocationHandler;
import com.xxx.mall.service.impl.OrderServiceImpl;
import com.xxx.mall.util.ProxyUtil;import java.lang.reflect.Proxy;public class Client {public static void main(String[] args) {// 创建目标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);// 调用代理对象的代理方法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}
CGLIB动态代理
CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
引入依赖:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
一个没有实现接口的类:
package com.xxx.mall.service;public class UserService {public void login(){System.out.println("用户正在登录系统....");}public void logout(){System.out.println("用户正在退出系统....");}
}
使用CGLIB在内存中为UserService类生成代理类,并创建对象:
package com.xxx.mall;import com.xxx.mall.service.UserService;
import net.sf.cglib.proxy.Enhancer;public class Client {public static void main(String[] args) {// 创建字节码增强器// 这个对象时CGLIB库当中的核心对象,就是依靠它来生成代理类Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接口enhancer.setCallback(方法拦截器对象);// 生成源码,编译class,加载到JVM,并创建代理对象// 在内存中生成UserService类的子类,起始就是代理类的字节码;创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}
和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:
package com.xxx.mall.service;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class TimerMethodInterceptor implements MethodInterceptor {private Object target; // 目标对象public MethodInterceptorImpl(Object object){this.target= object;}@Overridepublic Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return null;}
}
MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:
- 第一个参数:代理对象
- 第二个参数:目标方法
- 第三个参数:目标方法调用时的实参
- 第四个参数:代理方法
在MethodInterceptor的intercept()方法中调用目标以及添加增强:
package com.xxx.mall.service;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 前增强long begin = System.currentTimeMillis();// 调用目标Object retValue = methodProxy.invokeSuper(proxy, objects);// 后增强long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");// 一定要返回return retValue;}
}
修改客户端程序:
package com.xxx.mall;import com.xxx.mall.service.TimerMethodInterceptor;
import com.xxx.mall.service.UserService;
import net.sf.cglib.proxy.Enhancer;public class Client {public static void main(String[] args) {// 创建字节码增强器Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接口enhancer.setCallback(new TimerMethodInterceptor());// 生成源码,编译class,加载到JVM,并创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}
对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:
- –add-opens java.base/java.lang=ALL-UNNAMED
- –add-opens java.base/sun.net.util=ALL-UNNAMED
面向切面编程AOP
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。AOP是一种编程技术,AOP是对OOP的补充延伸。AOP底层使用的就是动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
一个系统的系统服务,如:日志、事务管理、安全等,这些又被称为:交叉业务。交叉业务几乎是通用的,如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
- 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
使用AOP解决以上问题:
AOP::将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
- 代码复用性增强。
- 代码易维护
- 使开发者更关注业务逻辑。
AOP的七大术语
- 连接点 Joinpoint: 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。连接点描述的是位置。
- 切点 Pointcut: 在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
- 通知 Advice: 通知又叫增强,就是具体的代码,例如事务代码、日志代码等。包括:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
- 切面 Aspect: 切点 + 通知就是切面。
- 织入 Weaving: 把通知应用到目标对象上的过程。
- 代理对象 Proxy: 一个目标对象被织入通知后产生的新对象。
- 目标对象 Target: 被织入通知的对象。
切点表达式: 切点表达式用来定义通知(Advice)往哪些方法上切入。
语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
- 可选项。
- 没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
返回值类型:
- 必填项。
- *表示返回值类型任意。
全限定类名:
- 可选项。
- 两个点“…”代表当前包以及子包下的所有类。
- 省略时表示所有的类。
方法名:
- 必填项。
- *表示所有方法。
- set*表示所有的set方法。
形式参数列表:
- 必填项
- () 表示没有参数的方法
- (…) 参数类型和个数随意的方法
- (*) 只有一个参数的方法
- (*, String) 第一个参数类型随意,第二个参数是String的。
异常:
- 可选项。
- 省略时表示任意异常类型。
举例:
- execution(public * com.powernode.mall.service.*.delete*(…)):service包下所有的类中以delete开始的所有方法
- execution(* com.powernode.mall…*(…)):mall包下所有的类的所有的方法
- execution(* *(…)):所有类的所有方法
使用Spring的AOP
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
实际开发中,都是Spring+AspectJ来实现AOP。
AspectJ:(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ) AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
准备工作
1)引入相关的jar包:
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.1.0-M2</version></dependency><!--spring aop依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.1.0-M2</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.1.0-M2</version></dependency>
2)修改配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns: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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
基于AspectJ的AOP注解式开发
第一步:定义目标类以及目标方法:
package com.xxx.spring6.service;import org.springframework.stereotype.Service;@Service("userService")
public class UserService { //目标类// 目标方法public void login(){System.out.println("系统正在进行身份认证……");}
}
第二步:定义切面类
package com.xxx.spring6.service;import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;@Component("logAspect")
public class LogAspect { // 切面// 切面 = 切点 +通知}
第三步:目标类和切面类都纳入spring bean管理
在目标类UserService上添加@Service注解。
在切面类LogAspect类上添加@Component注解。
第四步:在spring配置文件中添加组建扫描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns: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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--组件扫描--><context:component-scan base-package="com.xxx.spring6.service"/></beans>
第五步:在切面类中添加通知
package com.xxx.spring6.service;import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;@Component("logAspect")
public class LogAspect { // 切面// 切面 = 切点 +通知// 通知就是增强,就是具体的要编写的增强代码// 这里通知Advice以方法的形式出现public void advice(){System.out.println("我是一个通知,这是一个增强代码……");}}
第六步:在通知上添加切点表达式
@Aspect // 切面类需要用@Aspect标注,若不标注,则不认为是切面类
@Component("logAspect")
public class LogAspect { // 切面// 切面 = 切点 +通知// 通知就是增强,就是具体的要编写的增强代码// 这里通知Advice以方法的形式出现//切点表达式// @Before注解标注的方法就是一个前置通知@Before("execution(* com.xxx.spring6.service.UserService.*(..))")public void advice(){System.out.println("我是一个通知,这是一个增强代码……");}}
第七步:在spring配置文件中启用自动代理
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns: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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--组件扫描--><context:component-scan base-package="com.xxx.spring6.service"/><!--开启aspectj的自动代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
<aop:aspectj-autoproxy proxy-target-class=“true”/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。
proxy-target-class=“true” 表示采用cglib动态代理。
proxy-target-class=“false” 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。
测试程序:
@Testpublic void testBefore(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");UserService userService = applicationContext.getBean("userService", UserService.class);userService.login();}
结果:
我是一个通知,这是一个增强代码……
系统正在进行身份认证……
通知类型包括:
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
测试举例
package com.xxx.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;/*** ClassName: LogAspect* PackageName: com.xxx.spring6.service* Description:** @Author Xiyan Zhong* @Create 2023/10/30 下午5:32* @Version 1.0*/
@Aspect
@Component("logAspect")
public class LogAspect { // 切面// 前置通知@Before("execution(* com.xxx.spring6.service..*(..))")public void beforeAdvice() {System.out.println("前置通知");}// 后置通知@AfterReturning("execution(* com.xxx.spring6.service..*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}// 环绕通知 是最大的通知,在前置通知之前,在后置通知之后@Around("execution(* com.xxx.spring6.service..*(..))")public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {// 前面的代码System.out.println("环绕前面的通知");// 执行目标joinPoint.proceed();//// 后面的代码System.out.println("环绕后面的通知");}// 异常通知@AfterThrowing("execution(* com.xxx.spring6.service..*(..))")public void afterThrowingAdvice(){System.out.println("异常通知");}// 最终通知 finally语句块中的通知@After("execution(* com.xxx.spring6.service..*(..))")public void afterAdvice(){System.out.println("最终通知");}}
业务层代码:
package com.xxx.spring6.service;import org.springframework.stereotype.Service;@Service("orderService")
public class OrderService {public void generate() {System.out.println("生成订单……");////if (1 == 1) {// throw new RuntimeException("运行时异常……");//}}
}
若业务层不发生异常:
环绕前面的通知
前置通知
生成订单……
后置通知
最终通知
环绕后面的通知
若业务层发生异常:
环绕前面的通知
前置通知
生成订单……
异常通知
最终通知java.lang.RuntimeException: 运行时异常……
当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。出现异常之后,后置通知和环绕通知的结束部分不会执行。
前面的先后顺序
业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
举例:
package com.xxx.spring6.service;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Aspect
@Component("securityAspect")
@Order(1)
public class SecurityAspect { //安全切面@Before("execution(* com.xxx.spring6.service..*(..))")public void beforeAdvice(){System.out.println("安全前置通知");}
}
package com.xxx.spring6.service;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Aspect
@Component("logAspect")
@Order(2)
public class LogAspect { //安全切面@Before("execution(* com.xxx.spring6.service..*(..))")public void beforeAdvice(){System.out.println("前置通知");}
}
输出:
安全前置通知
前置通知
生成订单……
以上使用切点的缺点:
- 切点表达式重复写了多次,没有得到复用。
- 如果要修改切点表达式,需要修改多处,难维护。
优化:定义一个通用切点的方法,用其代替切点表达式,注意该方式能跨切面使用,举例如下:
package com.xxx.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;@Aspect
@Component("logAspect")
@Order(2)
public class LogAspect { // 切面// 定义通用的切点表达式@Pointcut("execution(* com.xxx.spring6.service..*(..))")public void commonPointcut(){}// 前置通知//@Before("execution(* com.xxx.spring6.service..*(..))")@Before("commonPointcut()")public void beforeAdvice() {System.out.println("前置通知");}// 后置通知@AfterReturning("commonPointcut()")public void afterReturningAdvice(){System.out.println("后置通知");}// 环绕通知 是最大的通知,在前置通知之前,在后置通知之后@Around("commonPointcut()")public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {// 前面的代码System.out.println("环绕前面的通知");// 执行目标joinPoint.proceed();//// 后面的代码System.out.println("环绕后面的通知");}// 异常通知@AfterThrowing("commonPointcut()")public void afterThrowingAdvice(){System.out.println("异常通知");}// 最终通知 finally语句块中的通知@After("commonPointcut()")public void afterAdvice(){System.out.println("最终通知");}}
package com.xxx.spring6.service;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Aspect
@Component("securityAspect")
@Order(1)
public class SecurityAspect { //安全切面//@Before("execution(* com.xxx.spring6.service..*(..))")@Before("com.xxx.spring6.service.LogAspect.commonPointcut()")public void beforeAdvice(){System.out.println("安全前置通知");}
}
输出:
安全前置通知
环绕前面的通知
前置通知
生成订单……
后置通知
最终通知
环绕后面的通知
连接点 joinPoint
在调用切面的方法时,Spring会自动传入连接点作为方法的实参(所有切点都可以使用),如:
@Before("commonPointcut()")public void beforeAdvice(JoinPoint joinPoint) {// JoinPoint joinPoint 在Spring容器调用这个方法的时候自动传过来,可以直接用// joinPoint 的作用:可获取目标方法的签名,通过方法的签名可以获取到方法的具体信息//Signature signature = joinPoint.getSignature();//例如——获取目标方法的方法名//String name = joinPoint.getSignature().getName();System.out.println("目标方法的方法名:"+ joinPoint.getSignature().getName());System.out.println("前置通知");}@AfterReturning("commonPointcut()")public void afterReturningAdvice(JoinPoint joinPoint){System.out.println("后置通知");}
全注解方式的AOP
全注解方式即不适用XML配置文件。
1)创建配置类:
package com.xxx.spring6.service;import org.springframework.cache.annotation.CacheConfig;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@CacheConfig // 代替XML文件
@ComponentScan({"com.xxx.spring6.service"}) // 配置组件扫描
@EnableAspectJAutoProxy(proxyTargetClass = true) // 启用aspectJ自动代理
public class Spring6Config {
}
- 测试:
@Testpublic void testNoXML(){ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);OrderService orderService = applicationContext.getBean("orderService", OrderService.class);orderService.generate();}
一个通知具有多个切点表达式举例——用逻辑运算符||进行拼接:
package com.xxx.spring6.biz;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Component
@Aspect
public class SecurityAspect {@Pointcut("execution(* com.powernode.spring6.biz..save*(..))")public void savePointcut(){}@Pointcut("execution(* com.powernode.spring6.biz..delete*(..))")public void deletePointcut(){}@Pointcut("execution(* com.powernode.spring6.biz..modify*(..))")public void modifyPointcut(){}@Before("savePointcut() || deletePointcut() || modifyPointcut()")public void beforeAdivce(JoinPoint joinpoint){System.out.println("XXX操作员正在操作"+joinpoint.getSignature().getName()+"方法");}
}
同理,上面的形式还可以写成: