Spring 整体学习(IOC容器 DI依赖注入 AOP 面向切面编程(动态代理) 配置注入 注解注入 )
Spring
一. Spring的IOC与DI
IOC(Invertion Of Control)是控制翻转,是一种编程的思想,就是以前在代码中我们需要通过 new 的方式来使用其他的对象,这种方式导致的结果就是类与类之间耦合度过高,耦合度高导致到迭代成本极高;通过控制的翻转的方式,就是我们项目中用到所有的对象,都通用交个一个容器来管理(在spring中我们叫做 IOC容器);
反转了依赖关系的满足方式,由之前的自己创建依赖对象,变为由工厂推送。(变主动为被动,即反转)
解决了具有依赖关系的组件之间的强耦合,使得项目形态更加稳健 DI(Dependency Injection)是依赖注入,它是对IOC编程思想的具体实现。在Spring创建对象的同时,为其属性赋值,称之为依赖注入。
1.1 依赖注入
依赖注入主要分为
属性注入(set方法注入)
和构造器注入
第一步,创建一个配置文件
application-context.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">
<!-- SSDDisk和MachineDisk实现了共同的 Disk 接口 -->
<!-- 当容器启动的时候会创建一个名为 ssdDisk 的对象,然后纳入到IOC容器中 -->
<bean id="ssdDisk" class="org.example.ioc.impl.SSDDisk"></bean>
<bean id="machineDisk" class="org.example.ioc.impl.MachineDisk"></bean>
<!-- primary=true 表示以该对象为主,当从容器中根据类型来获取的时候,拿到是这个对象 -->
<!-- 可以避免 NoUniqueBeanDefinitionException 异常的发生 -->
<bean id="computer" class="org.example.ioc.Computer" primary="true">
<!-- property 会找到 Computer类的 setDisk方法来将属性设置到对象中 -->
<property name="disk" ref="ssdDisk"></property>
<property name="name" value="联想电脑"></property>
</bean>
<!-- 通过构造器的方式来实现注入 -->
<bean id="computerTwo" class="org.example.ioc.Computer">
<constructor-arg name="disk" ref="ssdDisk"></constructor-arg>
<constructor-arg name="name" value="dell"></constructor-arg>
</bean>
</beans>
第二步,创建容器,并获取IOC容器中的内容:
IOC容器中的所有bean都会提前创建好,而不是调用的时候再去创建
public class IocTest {
public static void main(String[] args) throws InterruptedException {
// 创建一个 IOC 容器
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("META-INF/application-context.xml");
// 获取到IOC中的对象
// Computer c = (Computer)ctx.getBeam("computer");
// 可以根据类型来获取;因为目前对于上面的配置来说,我们有两个 Computer对象,拿到的是
// 加了 Primary = true 这个对象
Computer c = ctx.getBeam(Computer.class);
}
}
1.2 特殊类型注入
特殊类型指的是:
- 引用类型;
- List
- Map
- Set
- Propety
- 数组
例如给定如下的类:
public class Person {
private String name;
private Date birthday;
private List<Dog> dogs
private List<String> friends;
private Set<String> account;
private Map<String, String> addresses;
private Properties prop;
private String[] array;
// setter and getter
}
public class Dog {
private String name;
private Integer 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="birthday" class="java.util.Date"></bean>
<bean id="person" class="org.Person">
<property name="name" value="张三"></property>
<property name="birthday" ref="birthday"></property>
<property name="dogs">
<!-- List对象注入 -->
<List>
<bean class="org.Dog">
<property name="name" value="大黄"></property>
<property name="age" value="5"></property>
</bean>
<bean class="org.Dog">
<property name="name" value="小白"></property>
<property name="age" value="2"></property>
</bean>
</List>
</property>
<property name="friends">
<!-- list注入 -->
<list>
<value>李四</value>
<value>王五</value>
</list>
</property>
<property name="account">
<!-- set注入 -->
<set>
<value>李四</value>
<value>王五</value>
</set>
</property>
<property name="addresses">
<!-- map注入 -->
<map>
<entry name="home-address" value="洪山"></entry>
<entry name="home-address" value="江夏"></entry>
</map>
</property>
<property name="prop">
<!-- map注入 -->
<props>
<prop name="usename">root</prop>
<prop name="password">123456</prop>
</props>
</property>
<!-- 数组注入可以通过 , 将值进行分隔 -->
<!-- <property name="array" value="a,b,c"></property> -->
<property name="array">
<array>
<value>a</value>
<value>b</value>
<value>c</value>
</array>
</property>
</bean>
</beans>
1.3 自动装配
在 “配置” 的方式上有一个属性
autowire="byName | byType"
两种方式:
- byName 是严格按照名字到容器中进行查找,就算有primary="true"也会根据名字找。
- byType 是根据属性的类型到容器中进行查找,如果多个会报错,除非在其中一个加上 primary=“true” 这个属性
1.4 单例和非单例模式
在 “配置” 的方式上有一个属性
scope="singleton | prototype"
两种方式创建bean:
- singleton 表示从容器中获取的永远是单例的。
- prototype表示每次从容器中获取的都是一个新的对象。
二. bean的生命周期
关于 spring中管理的bean的生命周期在spring的一个接口
BeanFactory(是spring容器的*接口)
的描述上有准确的描述,但是目前来说,先简单理解 spring 的生命周期:
- 实例化,调用构造方法。
- 设置属性。
- 初始化,调用 init-method(配置的方式) 对应的方法
- 创建完毕。
- 销毁阶段,调用 destory-method(配置的方式) 对应的方法
public class LifeCycleBean {
private String name;
public void setName(String name) {
System.out.println("setName 方法被调用了...");
this.name = name;
}
public LifeCycleBean() {
System.out.println("构造方法被调用了...");
}
public void init() {
System.out.println("init 方法被调用了...");
}
public void destroy() {
System.out.println("destroy 方法被调用了...");
}
}
<?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">
<!--
对应的 org.LifeCycleBean 输入的结果:
1. 构造方法被调用了...
2. setName 方法被调用了...
3. init 方法被调用了...
4. destroy 方法被调用了...
-->
<bean id="lifeCycleBean" class="org.LifeCycleBean" init-method="init"
destroy-method="destroy">
<property name="name" value="XXX"></property>
</bean>
</beans>
单例bean:singleton
随工厂启动创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》随工厂关闭销毁
多例bean:prototype
被使用时创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》JVM垃圾回收销毁
三. 注入的注解形式开发
spring提供两套创建 IOC 容器的方式:
- 配置的方式,这是历史遗留问题。
- 注解的方式,这也是未来的主流的方式。(注解创建成员对象的方式是通过构造器)
在三阶段,我们都是混合使用;在四阶段(包括以后工作) 都是适应注解的方式。
@ComponentScan(basePackages = "com.qf")
public class IocTestWithAnnotation {
public static void main(String[] args) {
/**
* 通过注解的方式来创建 IOC容器,传递给 AnnotationConfigApplicationContext 的构造方法的
* 类会被实例化并纳入到IOC容器中。然后通过它头顶 @ComponentScan(basePackages = "com.qf") 递归
* 扫描 com.qf 下所有的类和接口,然后将他们头顶上带 @Service @Repository @Component 标注的类
* 按照spring-bean的生命的方式来创建后纳入到IOC容器中。
*/
ApplicationContext ctx = new AnnotationConfigApplicationContext(
IocTestWithAnnotation.class);
UserService userService = ctx.getBean(UserService.class);
userService.add();
userService.delete(45);
}
}
此处注解相当于配置文件中的
@Repository
: 这个注解放到 DAO 的实现类上。
@Sercvice
: 这个注解放到 Serviece 的实现类上。`
@Component
: 性质不明确的时候,就加上该注解。
此处注解相当于配置文件中的autowire="byType | byName "
@Autowired
:首先根据类型查找,如果多个,就看谁的头顶的有 @Primary,就注入谁;如果没有没有@Primary 注解,就按照名字来进行注入,如果名字都没有区分就报异常。
@Qualifier
: 它的出现就是为了和 @Autowire 搭配使用的,就是指定一个名字,在注入的时候根据类型的时候同时判断名字来实现注入,无视名字和@Primary注解,只根据@Qualifier来创建。
@Resource
:首先根据名字来实现注入,如果命名没有相同,就根据类型查找,如果类型有多个,就看谁的大头顶有 @Primary 注解,名字优先于@Primary。
此处的注解相当于init-method | destroy-method配置
@PostConstruct
: 对应着 init-method 这个配置的一个注解,是spring bean的生命周期的一个注解。在Spring中碰到方法名或者注解中带有 Post, 表示在XXXX之后。
@PreDestroy
: 对应着destroy-method这个配置的一个注解,销毁的时候执行的方法。在Spring中碰到方法名或者注解中带有 Pre, 表示在XXXX之前。
四. AOP
AOP(Aspect Oriented Programming): 面向切面的编程。说白就是可以在方法执行的某个时机做一些其他的工作;他是对应 OOP 的一种增强。AOP底层的实现都是通过动态代理(JDK原生方式和CGLIB的方式)来实现的,在spring框架中,提供了两种实现的方式:
- spring原生的方式。(了解即可)
- 通过AspectJ框架的方式。
面试:
OOP和AOP的区别?
答:OOP面向对象编程,只能通过纵向的添加的代码的方式来实现增强的功能;AOP可以实现在不改变原有方法的代码基础之上实现对方法的横向扩展。
连接点:类中方方法。
切入点:就是我们所关注的方法。
通知:就是当我们所关注的切入点,在发生某个行为的时候,出发某个调用。
4.1 AOP开发术语
- 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
- 切入点(Pointcut):被切入连接点。
- 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
- 目标对象(Target):代理的目标对象
- 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
- 代理(Proxy):被AOP织入通知后,产生的结果类。
- 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。
4.2 通配切入点
根据表达式通配切入点
<!--匹配参数-->
<aop:pointcut id="myPointCut" expression="execution(* *(com.qf.aaron.aop.basic.User))" />
<!--匹配方法名(无参)-->
<aop:pointcut id="myPointCut" expression="execution(* save())" />
<!--匹配方法名(任意参数)-->
<aop:pointcut id="myPointCut" expression="execution(* save(..))" />
<!--匹配返回值类型-->
<aop:pointcut id="myPointCut" expression="execution(com.qf.aaron.aop.basic.User *(..))" />
<!--匹配类名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop.basic.UserServiceImpl.*(..))" />
<!--匹配包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop.basic.*.*(..))" />
<!--匹配包名、以及子包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop..*.*(..))" />
4.3 AspectJ对于AOP的实现
第一步,创建一个接口,然后定义一个实现类。
public interface StudentService {
void deleteStudent(Integer id);
void add();
}
public class StudentServiceImpl implements StudentService {
public void deleteStudent(Integer id) {
System.out.println("删除学生:" + id);
}
@Override
public void add() {
System.out.println("add");
}
}
第二步,创建一个通知类,类的头顶必须要有 @Aspect 这个注解
@Aspect
public class AopAdvisor {
/**
* “* org.example.aop.service.impl.*.*(..)” 第一个 * org.example 表示人任意的返回值
* org.example.aop.service.impl.*.* org.example.aop.service.impl下所有的类的所有的方法。
* (..) 表示接受任意的参数
* @Before() 表示在执行这些方法之前。
*/
@Before("execution(* org.example.aop.service.impl.StudentServiceImpl.deleteStudent(..))")
public void before(JoinPoint jp) {
System.out.println("UserService下的方法调用之前被执行了...");
// 获取方法的签名
MethodSignature signature = (MethodSignature)jp.getSignature();
//获取方法的对象
Method method = signature.getMethod();
System.out.println("方法执行之前打印方法名: " + method.getName());
}
// 是方法无论正确获取不正确都会执行
@After("execution(* org.example.aop.service.impl.StudentServiceImpl.add*(..))")
public void after(JoinPoint jp) {
System.out.println("方法无论是否抛出异常都会执行");
}
// 方法正确执行了,才会执行该方法
@AfterReturning("execution(* org.example.aop.service.impl.StudentServiceImpl.add*(..))")
public void after(JoinPoint jp) {
System.out.println("方法正确执行了,才会执行该方法");
}
/**
* 1. 只有当关注的方法抛出 NullPointerException 这个异常方法才会执行;
* 2. 要保证 throwing的值和方法中异常的形参名要一致。
* 3. 如果想让该方法处理所有的异常,要使用 Exception
*/
@AfterThrowing(value = "execution(* org.example.service.impl.UserServiceImpl.add*(..))", throwing = "npe")
public void catchSpecifyException(JoinPoint jp, NullPointerException npe) {
System.out.println("方法抛出了 NPE 异常");
}
/**
* 表示环绕通知。
*/
@Around("execution(* org.example.service.impl.UserServiceImpl.add*(..))")
public void around(ProceedingJoinPoint pjp) throws Exception{
Object[] args = pjp.getArgs(); // 表示获取方法执行的所有的参数
//获取方法的签名
MethodSignature signature = (MethodSignature)pjp.getSignature();
//获得当前方法对象
Method method = signature.getMethod();
//通过方法获得字节码对象,然后通过字节码对象来获取简单类名
System.out.println(method.getDeclaringClass().getSimpleName());
Object obj = pjp.proceed(); // 执行目标对象的方法
// 做其他
return obj;
}
}
第三步,在配置文件中进行配置
<?xml version="1.0" encoding="UTF-8"?>
<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"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 被代理的类必须要纳入到IOC容器中 -->
<bean id="studentService" class="org.example.aop.service.impl.StudentServiceImpl"></bean>
<bean id="advisor" class="org.example.aop.AopAdvisor"></bean>
<!-- 表示开启 aspectj
可以通过 proxy-target-class="true" 来控制使用 CGLIB 的方式来实现代理,false表示使用原生JDK的方式来实现动态代理;
如果被代理的类没有接口,那么尽管指定使用jdk的代理方式,但是也还是会使用CGLIB的方式来实现动态代理。
-->
<aop:aspectj-autoproxy proxy-target-class="false"></aop:aspectj-autoproxy>
</beans>
五. spring与mybatis的整合
第一步,导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.9</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- 是mybatis和spring整合的中间包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- 连接池,功能和 druid 是一样的 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.3</version>
</dependency>
</dependencies>
第二步,配置
<?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-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 当通过配置文件的方式来创建容器的时候,会根据 base-package="org.example" 进行递归扫描: @Service @Component, 纳入到IOC容器中 -->
<context:component-scan base-package="org.example"></context:component-scan>
<!-- 配置 HikariCP 的连接池 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/qf?serverTimezone=Asia/Shanghai"></property>
<!-- 连接池中最大的连接的数量 -->
<property name="maximumPoolSize" value="20"></property>
<!-- 测试连接是否有效的 sql 语句 -->
<property name="connectionTestQuery" value="select 1"></property>
</bean>
<!--
之前在单独使用 MyBatis的时候,我们自己通过 mybatis-config.xml 核心配置文件来自己创建一个 SqlSessionFactory;
但是在和Spring整合的时候,将该项工作交给 Spring容器了。
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 配置数据源 -->
<property name="dataSource" ref="dataSource"></property>
<!--
"classpath:" 是spring提供的一个用来定位文件路径的缩写;
classpath:org/example/**/*.xml 扫描 org/example 下所有的后代包的 xml文件
-->
<property name="mapperLocations" value="classpath:org/example/**/*.xml"></property>
<!-- 设置分页插件 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<!-- 分页插件的配置 -->
<property name="properties">
<props>
<!-- 设置方言 -->
<prop key="helperDialect">mysql</prop>
<!-- 设置分页合理化 -->
<prop key="reasonable">true</prop>
</props>
</property>
</bean>
</array>
</property>
<!-- 配置日志打印的 -->
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"></property>
</bean>
</property>
</bean>
<!-- 将所有的 Mapper纳入到容器中
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 因为获取 Mapper需要需要通过 SqlSession, 这里我们直接提供工厂就可以了 -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<!-- 这是找那些 XXMapper 的基础包;只找接口; 需要和 annotationClass 配置使用;表示只找头顶有 @Mapper 注解的接口。-->
<property name="basePackage" value="org.example"></property>
<property name="annotationClass" value="org.apache.ibatis.annotations.Mapper"></property>
</bean>
</beans>
六. spring的单元测试
第一步,引入依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.3</version>
<scope>test</scope>
</dependency>
第二部,测试类的编写
@RunWith(SpringJUnit4ClassRunner.class) // 用于spring测试
@ContextConfiguration("classpath:META-INF/application-context.xml")
public class UserServiceTest {
@Autowired
private IUserService userServiceImpl;
@Test
public void getPageData() {
List<User> list = userServiceImpl.getPageData(2, 10);
list.forEach(u -> System.out.println(u.getEmail() + "#" + u.getName()));
}
@Test
public void validateTx() {
userServiceImpl.validateTx();
}
}
上一篇: python中update的基本使用
下一篇: Spring学习之使用注解配置AOP