文章目录
Spring Framework是Spring生态圈最基本的项目,是其他项目的根基
一:核心容器:
Spring 容器是 Spring 框架的核心,是用来管理对象的。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。
什么样的对象放入容器中:
- dao类
- service类
- controller类
- 工具类
什么样的对象不放入容器对象中: - servlet类,listener,filter类
- 实体的bean类(因为这些类需要动态的从数据库中获取)
IOC控制反转
控制反转IOC是一种思想,是一种概念,指的是将对象的创建,赋值,管理工作都交给代码之外的容器实现,也就是对象的创建是由其他的外部资源完成。不好理解?实际上我们使用的TomCat服务器就体现了这种思想:我们仅仅在web.xml文件中使用servlet标签注册了servlet,我们并没有做任何new Servlet的操作吧,所以Servlet是TomCat服务器创建的,所以TomCat也称为容器,只不过TomCat容器里面存放放的是Servlet对象,Listener,Filter对象
怎么由Spring容器实现创建对象呢?
(一)加入Spring依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
(二)添加测试的接口和实现类,写一个测试方法用于测试
(三)在项目的resources目录下配置Spring配置文件ApplicationContext.xml,其中定义的bean标签就是想要让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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--声明bean,就是告诉spring要创建某个类的对象
id:对象的自定义名称,唯一值,spring通过这个名称找到对象
class:类的全限定名称(不能是接口,因为spring是通过反射机制创建对象的,必须要使用类)-->
<bean id="OneService" class="icu.not996.service.servoiceImpl.OneServiceImpl"/>
<!--此声明意味着Spring完成了 OneService oneService=new OneServiceImp
Spring是把创建好的对象放入map集合中Spring中有一个map集合是用来存放对象的
类似于:
springMap.put(id的值,对象);
例: springMap.put("OneServiceImpl",new OneServiceImpl());
一个bean标签声明一个java对象
-->
</beans>
(四)使用Spring创建对象
@Test
public void TestSpring01(){
//使用Spring容器创建对象
//1.指定Spring配置文件的名称
String config="ApplicationContext.xml";
//2.创建表示Spring容器的对象,ApplicationContest
//ApplicationContext就表示Spring容器,通过容器对象使用其中的对象
//ClassPathApplicationContext() 表示通过类路径记载Spring配置文件
ApplicationContext ac=new ClassPathXmlApplicationContext(config);
//从容器中获取你要获取的对象,调用你想要调用的方法
OneService oneService = (OneService) ac.getBean("OneService");
//使用Spring创建好的对象
oneService.doSome();
}
通过运行我们就能发现:我们并没有主动去new OneServlet对象,但是却可以获取到该对象
DI
di的语法分类:
1.setter注入(设值注入)
Spring调用类的构造方法,在set方法中可以实现属性的赋值,80%都是使用的是set注入
基本数据类型使用value属性直接注入:
<bean id="" class="">
<property name="name" value="张三" />
<property name="age" value="1" />
</bean>
引用数据类型使用ref属性引入:
<bean id="mySchool" class="icu.not996.ba02.School">
<property name="name" value="城北小学"/>
<property name="address" value="山东菏泽曹县"/>
</bean>
<!--引用类型的set注入:Spring调用类的set方法
语法:
<bean id="xxx" class="yyy">
<property name="属性名称" ref="bean的id(对象的名称)"/>
</bean>
-->
<bean id="myStudent" class="icu.not996.ba02.Student">
<property name="name" value="小华"/>
<property name="age" value="8"/>
<property name="school" ref="mySchool"/>
</bean>
2.构造注入
Spring调用类的有参构造方法,创建对象.在构造方法中完成赋值
表示构造方法中的一个参数
标签属性:
name:表示构造方法的形参名
index:表示构造方法的参数的位置,参数从左往右位置是 0,1,2的顺序
value:构造方法的形参类型是简单类型的,使用value
ref:构造方法的形参类型是引用类型的使用ref
<!--使用name属性实现构造注入-->
<bean id="MySchool" class="">
<constructor-arg name="address" value="山东菏泽曹县"/>
<constructor-arg name="name" value="城北中学"/>
</bean>
<bean id="oneStudent" class="">
<constructor-arg name="age" value="10" />
<constructor-arg name="name" value="小华" />
<constructor-arg name="school" ref="MySchool"/>
</bean>
3.引用类型的自动注入
Spring框架根据某些规则,可以对引用类型的赋值,不需要手动为引用类型赋值了.但是只能是引用类型,不能是基本数据类型
自动注入的使用规Name则:通常是byName和byType
byName:按名称注入,java类中引用属性名和Spring容器中(配置文件) 的id名称一样且数据类型一致的这样的容器中的bean,spring能够赋值给引用类型
语法:
简单类型的赋值
bytType:按类型注入:java类中的引用类型和spring容器中(配置文件)的class属性是同源关系的,这样的bean能赋值给引用类型
同源:同一类的意思:
1.java类中引用类型的数据类型和bean中的class的值是一样的
2.java类中引用类型的数据类型和bean的class的值是父子类关系
3.java类中引用类型的数据类型和bean的class的值是接口和实现类的关系
二:纯注解开发Spring
使用纯注解的方式开发Spring能进一步简化Spring框架的开发
首先我们回顾看看Spring配置文件ApplicationContext.xml做了什么:其实就是告诉了框架Bean的位置,并且为他注入了属性值呗
Spring 同样提供了注解的方式来做这些事情:
(1)初始化一个Spring配置类
首先,注解要写在类、方法上方,所以我们需要先写一个Spring的配置类SpringConfig:
使用@Configuration注解告诉Spring,我是一个Spring配置类
@Configuration //此注解表示此类是一个Spring的配置类
public class SpringConfig {
}
(2)怎么使用在容器中创建bean?
Spring提供了四个注解:
- @Component 用于在容器中创建一个Bean
Spring还提供了@Componment注解的三个衍生注解,功能相同,适用在创建不同的bean对象上 - @Controller 用于表现层的bean定义
- @Service 用于业务层的bean定义
- @Repositiory 用于数据层的bean定义
示例:
@Repository("bookDao") //注解的value属性等同于使用配置文件中的 id
public class BookDaoImpl implements BookDao {
String userName;
String some;
public void show() {
System.out.println("BookDaoImpl show() run ~"+userName+"---"+some);
}
} //代码无意义,仅用于测试参考
但是这些注解Spring怎么知道去哪里加载呢?全部扫描一遍?所以在Spring配置类中应该指定需要加载bean的包
使用@ComponentScan注解,可以传入一个数组,格式如下:
@Configuration
@ComponentScan({"icu.not996.dao.impl","icu.not996.service.impl"})
public class SpringConfig {
}
(3)使用注解开发怎么设置依赖注入?
基本数据类型@Value注解
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("ZhangMeiyu")
String userName;
@Value("东北军阀")
String some;
public void show() {
System.out.println("BookDaoImpl show() run ~"+userName+"---"+some);
}
}
但是这种方式会导致程序的耦合性很强,属性值很难修改,所以我们会使用配置文件的方式优化:
引入配置文件内容
(1)写一个配置文件:bookDao.properties
内容为:
dao.username=ZhangMeiyu
dao.some=东北军阀
(2)告诉Spring此配置文件的位置:
在Spring配置类SpringConfig中加上注解:
@PropertySource({“dao.properties”}) //可以是数组的形式,可以写多个
(3) 在代码中使用${“key”}即可引用配置文件中的value
@Value("${dao.username}")
String userName;
引用数据类型@Autowired注解
@Autowired的使用大致和@Value相同,只不过@Autowired默认是ByType通过类型进行自动注入
如果容器中有多个相同的类型就要通过ByName了,实现方式是在@Autowired注解下添加上另一个注解:@Qualifier(" ")
@Service("bookService")
public class BookServiceImpl implements BookService {
@Autowired()
@Qualifier("bookDao")
BookDao bookDao;
......
}
(4) 使用Spring管理第三方Bean
在开发中,我们经常要会涉及到将第三方开发好的Bean对象放入容器中,这种情况怎么解决?总不能在第三方的源码上加上@Component吧。
例如我们要将DataSource接口实现类对象放入容器,方便我们随时间使用
Spring当然也为我们准备了解决方案:
@Bean 注解
实现方法:
(1)首先写一个独立的配置类
(2)在其中写一个方法,返回值为需要在容器中创建的bean的类型
(3)给该方法加入@Bean注解
(4)为对象的属性赋值:
基本数据类型:
将属性提取为成员变量,在成员变量使用@Value属性为其赋值
引用数据类型:
引用数据类型的方法较为特殊,将需要注入的引用数据类型写在方法的行参数位置
(5)在方法中创建出该属性对象,之后调用Set方法设置对象的属性即可
public class JdbcConfig {
@Value("jdbc.driverClassName")
private String druidDataSource;
@Value("jdbc.url")
private String driverClassName;
@Value("jdbc.username")
private String userName;
@Value("jdbc.password")
private String password;
@Bean
public DataSource dataSource(BookService bookService1){
bookService1.show();
DruidDataSource dds=new DruidDataSource();
dds.setDriverClassName(druidDataSource);
dds.setUrl(driverClassName);
dds.setUsername(userName);
dds.setPassword(password);
return dds;
}
}
三、AOP
1.概念
理解面向切面编程:
(1)需要在分析项目的时候,找出切面
(2)合理的安排切面的执行时间(在目标方法前,还是在目标方法之后)
(3)合理的安排切面的执行位置,在哪个类,哪个方法增加功能
AOP(Aspect Orient Programming),面向切面编程,面向切面编程是从动态的角度考虑程序的运行过程
AOP底层:采用动态代理模式实现的,采用了两种代理:JDK动态代理,与CGLIB的动态代理
AOP是动态代理的规范化,把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式去用动态代理
aop作用:
(1)在目标类不修改源代码的情况下,增加功能
(2)减少重复的代码
(3)专注于业务功能的实现
(4)解耦合:主要是业务功能和非业务功能(日志,事务)的解耦合
什么时候考虑使用aop技术:
(1)当要给一份系统中存在的类修改功能,但是原有的类功能不完善,你没有源代码,只能通过AOP增加功能
(2)要给项目中的多个类,增加一个相同的功能,使用aop
(3)使用aop的方式给方法增加事务,以及日志输出
AOP(Aspect Orient Programming),面向切面编程
Aspect:切面,给你的目标类增加的功能,就是切面,像纪录日志提交事务的操作就是切面
切面的特点:一般和业务的方法无关,非业务方法,都是可以独立使用功能的
Orient:面向 Programming:编程
术语:
(1)Aspect:切面,表示增强的功能,就是一堆代码,完成某一个功能,一般是非业务功能
常见的切面功能:日志,事务,统计信息,参数检查,权限验证
(2)JoinPoint:连接点,连接业务方法和切面的位置,在Spring AOP中就是某类中的业务方法
(3)Pointcut:切入点,指多个连接点方法的集合,是多个方法,这些方法都要加入切面功能
(4)目标对象:给哪个类增加功能,这个类就是目标对象
(5)Advice (通知):通知表示切面功能执行的时间
2.实现AOP
1.Spring:Spring在内部实现了aop规范,能做aop的工作 (实现方式是接口)
Spring主要在事务处理时使用aop
我们项目开发中很少使用Spring的aop实现,因为Spring中的aop比较笨重
2.AspectJ:一个开源的,专门做aop的框架,来自于Eclipse开源基金会
AspectJ是一个优秀的面向切面的框架,它拓展了Java语言,提供了强大的切面实现
Spring框架中集成了AspectJ,通过Spring就能使用AspectJ的功能
由于AspectJ的优秀,所以Spring欣然吸纳了AspectJ,用其来实现AOP,所以需要先导入AspectJ的依赖
(1)添加AspectJ依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
(2)通知类
通知是一个方法,不能单独存在,所以需要一个通知类,在其中书写通知方法:
首先通知类需要被Spring所加载,所以通知类上需要加注解:@Component
通知类还需要告诉Spring:我是一个通知类! 所以还需要加上注解:@Aspect
@Component //此注解是为了让spring去加载它(要在SpringConfig的注解@ComponentScan扫描范围内)
@Aspect //此注解是告诉spring此类是一个通知类,告诉spring,扫描到我之后,把我当做aop处理
public class MyAdvice {......}
此时Spring并不能去加载这些注解,需要告诉Spring:我有一个使用注解开发的AOP需要你了解一下,所以,你还需要给SpringConfig类加注解:
@EnableAspectJAutoProxy //告诉程序,是使用注解开发的aop
通知类中的方法分为两种:
(1)私有的空壳方法:用于定义切入点,也就是指定你要为哪个方法添加功能,方法中不实现任何功能
方法上添加注解@PointCut("")注解,在其中书写切入点表达式
切入点表达式
标准格式为: 动作关键字 (访问修饰符 返回值 包名.类/接口名.方法名 (参数名) 异常名)
- 动作关键字一般使用 execution 表示执行到指定切入点
- 访问修饰符和异常名可以省略
- *表示任意符号,可以单独出现,也可以匹配包名/类名/方法名出现
- 例如:execution (public * com.it1k..UserService.find (*))
- …表示连续的任意符号,可以独立出现,常用于简化包名和参数名的书写,例如可以表示任意参数/任意返回值类型
- +:专用于匹配子类类型
- 例如:execution(* *…Service+. (…)) 表示任意Service子类的任意方法
(2)通知方法:抽取了切入点需要的共性功能
@Pointcut("execution(* icu.not996.dao.BookDao.save (..))") //匹配BookDao中的save方法
private void pt(){ //私有方法,空壳方法,负责定义通知在哪里执行
}
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
前置通知 @Before 切入点代码执行前
后置通知:@After 切入点代码执行后
环绕通知@Arround 切入点代码执行前后
返回后通知@AfterReturning 切入点方法返回值结果之后
抛出异常后@AfterThrowing 切入点方法抛出异常后
以上注解的value属性需要填入一个上述的私有方法名,表示在哪里执行
//指定一个执行时机,在切入点之前执行
@Around(value = "pt()")
public Object method1(ProceedingJoinPoint pjp){.....}
在方法运行开始之后的通知类型能获取到方法的传入的参数,通过在方法的形参位置添加一个对象JoinPoint,如果环绕通知为其子类ProceedingJoinPoint(因为在环绕通知中要指定在什么时机运行原始方法),获取到原始方法的参数之后能对参数进行校验
@AfterRunning和@AfterThrowing和@Arround注解能拿到方法的返回值,对返回值进行一些操作再将其返回,此操作需要在注解的参数上使用
returning 指定返回值值,然后便可以在方法的形参位置写入该返回值供方法内部操作
举例:
@AfterReturning(value="ptDel()",returning = "rtv")
public Object afterReturning(JoinPoint pjp,Object rtv) throws Throwable {
Object[] args = pjp.getArgs();
Arrays.stream(args).forEach(arg-> System.out.println("方法参数为:"+arg));
System.out.println(rtv.toString()+"条数据被删除");
return rtv;
}
环绕通知示例:
@Around(value = "pt()")
public Object method1(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
Object[] args = pjp.getArgs();//获取原始方法的参数
if(("高质量男性的自身修养").equals(((Book)(args[0])).getName())){ //参数校验
((Book)(args[0])).setName("人间油物");//对原始数据的值进行修改,默认会将传入的值直接执行
}
System.out.println("程序执行前系统时间为:"+System.currentTimeMillis());//添加通知
Object proceed = pjp.proceed(args);//运行原始方法,并获取原始方法的返回值
if(Integer.parseInt(proceed.toString())>0){
System.out.println("数据添加成功");
}else{
System.out.println("数据添加失败");
}
return Integer.parseInt(proceed.toString());//将原始方法的返回值加工后返回
}
所以环绕通知还是很强大的,可以实现其他四种通知的功能
四、事务
关于事务的概念不多介绍,在Spring中实现事务是在数据层和业务层保障一系列数据库操作同时成功或者同时失败。
Spring实现事务是通过Spring提供的接口:PlatformTransactionManger
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException; //事务提交
void rollback(TransactionStatus var1) throws TransactionException; //事务回滚
}
参考:Spring知识梳理
Spring注解开发实现事务:
(1)在业务层接口上添加Spring事务管理:
使用@Transactional注解
public interface AccountService {
/**
* 模拟转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
(2)设置事务管理器
写一个返回值为PlatfornTransactionManager(面向接口编程)的方法,方法可写在JdbcConfig类当中,传入参数为DataSource实现引用类型的自动注入
//配置事务管理器,使用的是jdbc事务
@Bean //让Spring去加载这个Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
(3) 在Spring配置类上开启注解式事务驱动
@Configuration
@ComponentScan("xxxx.xxxxx")
@PropertySource("classpath:xxxx.properties")
@Import({xxxx.class,xxxxx.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
这样,一个基础的Spring事务Demo就写完了
实现原理:
@Transactional注解会开启一个Spring事务,原本数据库的两个update操作同样也会各自开启一个事务,所以目前存在三个事务。Spring会做如下操作:在我的控制范围中所有的事务都要加入我开启的事务,Spring开启的事务被称为事务管理员,加入的事务成为事务协调员,此时程序中便只有一个事务。该事务中代码出现错误,整个事务就会回滚。
Spring是怎样实现介入dao操作的呢?因为在上面事务管理器中传入了一个DataSource对象,该对象要和实现数据增删改的DataSource是同一个对象。
事务的属性
这些属性都可以在@Transactional注解上开启
rollbackFor:Spring默认回滚的是Error和RunnTimeException 其他异常,编译时异常默认不进行回滚,有相关业务操作要手动加上该异常:
@Transactional(rollbackFor = {NullPointerException.class})
事务的传播行为
事务的传播行为:事务协调员对事务管理员所携带事务的处理态度(即事务管理员开启事务,事务协调员加入事务/不加入事务/重新开启事务)
配置事务的传播行为是一个枚举值
应用场景:如果有一个操作不想加入事务管理员,无论事务是否执行失败,该操作都要执行。例如:记录日志操作
这就要求日志开启的事务不要加入Spring开启的事务,此时就需要在日志操作方法上加上日志的传播行为参数:
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
此时无论事务执行成功还是失败,都不影响日志的执行
菜鸟,欢迎大家指正