文章目录
- 1.什么是Spring框架,Spring框架有哪些主要模块?
- 2.什么是控制反转(IoC),什么是依赖注入?
- 3.在java中依赖注入有哪些方式?
- 4.BeanFactory和ApplicationContext有什么区别?
- 5.Spring提供几种配置方式来设置元数据
- 6.如何使用XML配置方式配置Spring
- 7.怎样用注解的方式配置Spring
- 8.请解释Spring Bean的生命周期
- 9.Spring Bean作用域的区别是什么
- 10.Spring中的单例Bean是线程安全的吗
- 11.请举例说明如何在Spring中注入一个Java集合
- 12.请解释Spring Bean的自动装配
- 13.如何向Spring Bean中注入java.util.Properties
- 14.自动装配有哪些局限性
- 15.请解释各种自动装配模式的区別
- 16.请举例解释@Required注解
- 17.请举例解释@Qualifier注解
- 18.构造方法注入和设值注入有什么区別
- 19.Spring中有哪些不同类型的事件
- 20.FileSystemResource和ClassPathResource有什么区別
- 21.Spring中用到了那些设计模式
- 22.在Spring中如何更有效地使用JDBC
- 23.请解释Spring中的IoC容器
- 24.在Spring中可以注入null 或空字符串吗
- 25.Spring如何处理线程并发问题?
- 26.什么是Spring的inner beans
- 27.@Autowired和@Resource之间的区别
- 28.Spring支持的事务管理类型?spring 事务实现方式有哪些?
- 29.说一下Spring的事务传播行为
- 30.说一下 spring 的事务隔离?
- 31.什么是AOP
- 32.JDK动态代理和CGLIB动态代理的区别
- 33.解释一下Spring AOP里面的几个名词
- 34.Spring通知有哪些类型?
- 35.AOP+注解实现日志入参出参打印
- 36.spring循环依赖
1.什么是Spring框架,Spring框架有哪些主要模块?
Spring是一个轻量级Java开发框架,最早有Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。
Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。
Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。
- spring core:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
- spring beans:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
- spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法。
- spring jdbc:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析, 用于简化JDBC。
- spring aop:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
- spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc 容器初始化和针对Web的ApplicationContext。
- spring test:主要为测试提供支持的,支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。
2.什么是控制反转(IoC),什么是依赖注入?
-
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
-
Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
-
Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
-
DI—Dependency Injection,即“依赖注入”:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
3.在java中依赖注入有哪些方式?
构造方法注入
- 构造方法注入:保证了一些必要的属性在Bean实例化时就设置,并且确保了bean实例在实例化后就可以使用.
- 在类中,不用为属性设置setter方法,只需提供构造方法即可
- 在构造文件中配置该类bean,并配置构造器,在配置构造器中用
//ApplicationContext.xml
<bean id="action" class="com.action.UserAction">
<constructor-arg index ="0" name="name" value="Murphy"></constructor-arg>
</bean>
提供构造方法
public class UserAction {
private String name;
public UserAction(String name) {
this.name = name;
}
}
setter注入
- 根据property标签的name属性的值去找对应的setter方法.
例如: name= “aa” 对应的就是setAa方法. - 由于属性注入具有可选性和灵活性高的优点,是实际上最常用的注入方式.
- 属性注入要求bean提供一个默认的构造函数,并为需要注入的属性提供对应的setter方法.spring先调用bean默认的构造函数实例化bean对象,然后通过反射机制的方法调用setter方法注入属性值.
- 还有一点需要注意:如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。
//ApplicationContext.xml
<bean id="action" class="com.action.UserAction">
<property name="name" value="Murphy"/>
</bean>
提供setting方法
public class UserAction {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
注解注入
@Autowired(构造,接口,方法) :自动装配,默认根据类型注入
属性Required
- @Autowired(required=true):当使用@Autowired注解的时候,其实默认就是@Autowired(required=true),表示注入的时候,该bean必须存在,否则就会注入失败
- @Autowired(required=false):表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错。
- required属性含义和@Required一样,只是@Required只适用于基于XML配置的setter注入方式,只能打在setting方法上。
public class AutowiredAction {
private String name;
private List<String> list;
@Autowired
private AutowiredAction(String name) {
this.name=name;
}
public String getName() {
return name;
}
@Autowired
public void setName(String name) {
this.name = name;
}
@Autowired(required = true)
private void initName(String name,List<String> list) {
this.name = name;
this.list = list;
}
}
接口
public interface AutowiredIn {
@Autowired
void initName(String name,Integer age);
}
@Resource 默认按照名称装配
- 可以标注在字段或属性的setter方法上。默认按照字段的名称去Spring容器中找依赖对象,如果没有找到,退回到按照类型查找
- 如果配置了属性name,那么只能按照名称找依赖对象
public class ResourceAction {
@Resource(name="name")
private String name;
@Resource
private List<String> list;
public String getName() {
return name;
}
@Resource
public void setName(String name) {
this.name = name;
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
}
@Autowired和@Resource区别
-
来源不同
@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案)。 -
支持的参数不同
@Autowired 和 @Resource 在使用时都可以设置参数
@Resource 注解设置 name 和 type 参数,实现代码如下:
@Resource(name = "userinfo", type = UserInfo.class)
private UserInfo user;
@Autowired 只支持设置一个 required 的参数
-
依赖注入的支持不同
@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入
-
依赖查找顺序不同
依赖注入的功能,是通过先在 Spring IoC 容器中查找对象,再将对象注入引入到当前类中。而查找有分为两种实现:按名称(byName)查找或按类型(byType)查找,其中 @Autowired 和 @Resource 都是既使用了名称查找又使用了类型查找,但二者进行查找的顺序却截然相反。
@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找,它的具体查找流程如下:
@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找,它的具体流程如下图所示:
4.BeanFactory和ApplicationContext有什么区别?
-
BeanFactory 可以理解为含有bean集合的工厂类。BeanFactory 包含了种bean的定义,以便在接收到客户端请求时将对应的bean实例化。
-
BeanFactory还能在实例化对象的时生成协作类之间的关系。此举将bean自身与bean客户端的配置中解放出来。BeanFactory还包含了bean生命周期的控制,调用客户端的初始化方法(initialization methods)和销毁方法(destruction methods)。
-
从表面上看,ApplicationContext如同BeanFactory 一样具有bean定义、bean关联关系的设置,根据请求分发bean的功能。但ApplicationContext在此基础上还提供了其他的功能。
- 1.提供了支持国际化的文本消息
- 2.统一的资源文件读取方式
- 3.已在监听器中注册的bean的事件
-
以下是三种较常见的 ApplicationContext 实现方式:
-
1、ClassPathXmlApplicationContext:从classpath的XML配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。
-
2、FileSystemXmlApplicationContext :由文件系统中的XML配置文件读取上下文。
-
3、XmlWebApplicationContext:由Web应用的XML文件读取上下文。
-
-
两者装载bean的区别
-
BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;
-
ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;
-
5.Spring提供几种配置方式来设置元数据
-
基于xml配置(数据源之类的)
-
基于注解(自动装配,bean之间的依赖关系保存在了BeanDefiion中)
-
基于java(手动装配,手动把依赖的bean装配进去)
6.如何使用XML配置方式配置Spring
7.怎样用注解的方式配置Spring
8.请解释Spring Bean的生命周期
根据图示的生命周期,具体过程如下:
(1)当调用者调用getBean(beanName)向容器请求某一个Bean时,如果容器注册了InstantiationAwareBeanPostProcessor接口,则在实例化Bean之前,将调用接口的postProcessBeforeInstantiation()方法。
(2)根据配置情况调用Bean的构造函数或工厂方法实例化Bean。
(3)如果容器注册了InstantiationAwareBeanPostProcessor接口,在实例化Bean之后,调用该接口的postProcessBeforeInstantiation()方法,对实例化的对象进行一些初始化设置。
(4)如果Bean 配置了属性信息,那么容器在这一步将配置的属性设置到Bean对应的属性中,在设置属性之前将先调用InstantiationAwareBeanPostProcessor接口的postProcessPropertyValues()方法。
(5)调用Bean中的setXxx()方法设置属性。
(6)如果Bean实现了BeanNameAware接口,则会调用该接口的setBeanName()方法,将配置文件中该Bean对应的名称设置到Bean中。
(7)如果Bean实现了BeanFactoryAware接口,则会调用该接口的setBeanFactory()方法,将BeanFactory容器实例设置到Bean中。
(8)如果BeanFactory装配了BeanPostProcessor后处理器,则将调用BeanPostProcessor接口的Object postProcessBeforeInitialization方法对Bean进行加工操作。其中,传入的bean是当前正在处理的bean,而beanName是当前Bean在配置文件中的配置名,返回加工处理后的bean。用户可以使用该方法对某些Bean进行特殊的处理。Spring容器所提供的AOP、动态代理等功能都通过BeanPostProcessor实现。
(9)如果Bean实现了InitializingBean接口,则将调用该接口的afterPropertiesSet()方法。
(10)如果在配置文件的bean标签中通过init-method属性指定了初始化方法,则会执行这个方法。
(11)如果BeanFactory装配了BeanPostProcessor后处理器,在这里则会执行Object postProcessAfterInitialization()方法对Bean进行加工处理。
BeanPostProcessor后处理器定义了两个方法,分别是postProcessBeforeInitialization()和postProcessAfterInitialization(),分别在第8步和在此处调用。
(12)如果在配置文件中指定了bean的scope=”prototype”,意味着配置的这个bean是多例的,每次获取该bean都会返回一个新实例化的bean,所以在这一步之后Spring容器不再管理多例的Bean,直接将当前生成的实例返回给用户。
对于scope=”singleton”的Bean(默认情况),意味着这个Bean是单例的,就需要把这个Bean缓存到在Spring IOC容器中,用户每次获取时都从这个容器中获取,并且Spring会对这些Bean进行后续的生命周期管理。
(13)对于单例的Bean,容器关闭时,会触发Spring对Bean的后续生命周期的管理工作。如果Bean实现了DisposableBean接口,则会调用该接口的destroy()方法,在这里可以进行释放资源、记录日志等操作。
(14)对于单例的Bean,如果在配置文件中指定了destroy-method属性,Spring则会执行这个属性配置的方法,完成Bean资源释放等操作。
9.Spring Bean作用域的区别是什么
- singleton : bean在每个Spring ioc 容器中只有一个实例。
- prototype:一个bean的定义可以有多个实例。
- request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
- session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
- global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
- 注意: 缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考,因为频繁创建和销毁 bean 会带来很大的性能开销。
10.Spring中的单例Bean是线程安全的吗
不是,Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
- 有状态就是有数据存储功能。
- 无状态就是不会保存数据。
11.请举例说明如何在Spring中注入一个Java集合
12.请解释Spring Bean的自动装配
在Spring框架中,在配置文件中设定bean的依赖关系是一个很好的机制,Spring 容器能够自动装配相互合作的bean,这意味着容器不需要和配置,能通过Bean工厂自动处理bean之间的协作。这意味着 Spring可以通过向Bean Factory中注入的方式自动搞定bean之间的依赖关系。自动装配可以设置在每个bean上,也可以设定在特定的bean上。
13.如何向Spring Bean中注入java.util.Properties
14.自动装配有哪些局限性
自动装配的局限性是:
重写:你仍需用 和 配置来定义依赖,意味着总要重写自动装配。
基本数据类型:你不能自动装配简单的属性,如基本数据类型,String字符串,和类。
模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。
15.请解释各种自动装配模式的区別
16.请举例解释@Required注解
17.请举例解释@Qualifier注解
18.构造方法注入和设值注入有什么区別
19.Spring中有哪些不同类型的事件
-
上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
-
上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
-
上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
-
上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
-
请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
20.FileSystemResource和ClassPathResource有什么区別
21.Spring中用到了那些设计模式
- 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
- 单例模式:Bean默认为单例模式。
- 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
- 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
- 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
22.在Spring中如何更有效地使用JDBC
使用Spring JDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate
23.请解释Spring中的IoC容器
24.在Spring中可以注入null 或空字符串吗
可以。
25.Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
26.什么是Spring的inner beans
在Spring框架中,当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean。内部bean可以用setter注入“属性”和构造方法注入“构造参数”的方式来实现,内部bean通常是匿名的,它们的Scope一般是prototype。
27.@Autowired和@Resource之间的区别
@Autowired可用于:构造函数、成员变量、Setter方法
@Autowired和@Resource之间的区别
-
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
-
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
28.Spring支持的事务管理类型?spring 事务实现方式有哪些?
Spring支持两种类型的事务管理:
-
编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
-
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
29.说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
30.说一下 spring 的事务隔离?
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
-
ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
-
ISOLATION_READ_UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
-
ISOLATION_READ_COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
-
ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
-
ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
31.什么是AOP
OOP(Object-Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect-Oriented Programming),一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
32.JDK动态代理和CGLIB动态代理的区别
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
-
JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
-
如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):
proxy是最终生成的代理实例;
method 是被代理目标实例的某个具体方法;
args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
33.解释一下Spring AOP里面的几个名词
(1)切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。 在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @AspectJ 注解来实现。
(2)连接点(Join point):指方法,在Spring AOP中,一个连接点 总是 代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
(3)通知(Advice):在AOP术语中,切面的工作被称为通知。
(4)切入点(Pointcut):切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
(5)引入(Introduction):引入允许我们向现有类添加新方法或属性。
(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。它通常是一个代理对象。也有人把它叫做 被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。
(7)织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入:
- 编译期:切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
34.Spring通知有哪些类型?
在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning ):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
35.AOP+注解实现日志入参出参打印
最终效果
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - top.gmaya.gmayaserviceadminimpl.system.controller.UserController.add()【新增登录信息】:===================
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - top.gmaya.gmayaserviceadminimpl.system.controller.UserController.add()【方法请求参数为】:{"data":{"id":1,"name":"1212"};,"token":"11111111"};
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - top.gmaya.gmayaserviceadminimpl.system.controller.UserController.add()【方法返回结果为】:{"msg":"success","code":0,"data":1};
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - top.gmaya.gmayaserviceadminimpl.system.controller.UserController.add()【方法执行时长为】:208 ms
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - {"method":"POST","createTime":1595487009526,"ip":"192.168.21.1","methodName":"add","className":"top.gmaya.gmayaserviceadminimpl.system.controller.UserController","createUser":"GMaya","time":208,"operation":"新增登录信息","url":"http://localhost:9001/user/add"};
[2020-07-23 14:50:09 INFO http-nio-9001-exec-2] t.g.gmayaserviceadminimpl.system.aspect.LogAspect - top.gmaya.gmayaserviceadminimpl.system.controller.UserController.add()【保存数据库成功!】:===================
思路:
一个注解类, 一个切面类。使用环绕通知,将入参出参打印出来,可以根据实际情况,有些接口只需要打印即可,有些需要打印并保存到数据库。
注解类GmLog
/**
* 自定义日志注解
* 1.运行时 使用使用注解
* 2.注解作用于方法上
* 3.注解是否将包含在 JavaDoc 中
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD };)
@Documented
public @interface GmLog {
// 打印日志描述信息
String value() default "";
// TODO 是否保存到数据库
boolean isSave() default false;
};
切面类LogAspect
里面有两种方式,一个是拦截添加注解的方法(适合新建项目,写接口的时候加上注解),一个是指定的包名下面所有的接口(适合现有项目,不必要改变其余代码)
/**
* 用于记录注解上接口的入参出参,统一规范。
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* 方法一: 不需要自定义注解, 直接拦截所有controller的请求。全部打印
* 定义切入点表达式
* 第一个*号:表示返回类型, *号表示所有的类型。
* 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包.
* 第二个*号:表示类名,*号表示所有的类。
* *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
*/
@Pointcut("execution(public * top.gmaya.gmayaserviceadminimpl.system.controller..*.*(..))")
public void privilege() {
};
/**
* 方法二:拦截该注解标识的方法
*/
@Pointcut("@annotation(top.gmaya.gmayaserviceadminimpl.system.annotation.GmLog)")
public void logPointCut() {
};
/**
* 环绕通知
* @param pjd
* @return
* @throws Throwable
*/
// @Around("privilege()") // 第一种方式
@Around("logPointCut()") // 第二种方式
public Object arount(ProceedingJoinPoint pjd) throws Throwable {
long startTime = System.currentTimeMillis();
// 类名
String className = pjd.getTarget().getClass().getName();
// 获取执行的方法名称
String methodName = pjd.getSignature().getName();
// 1. 如果是使用的第二种方式,则判断该方法是否使用了改注解
// 2. 如果是使用的第一种方式,直接注释即可。
GmLog gmLog = this.getAnnotationLog(pjd);
if (gmLog != null) {
String value = gmLog.value();
log.info("{};.{};()【{};】:===================", className, methodName, value);
};
Object[] args = pjd.getArgs();
try {
String params = JSON.toJSONString(args[0]);
//打印请求参数参数
log.info("{};.{};()【方法请求参数为】:{};", className, methodName, params);
}; catch (Exception e) {
log.info("{};.{};()【方法请求参数打印失败】:{};", className, methodName, e);
};
// 执行目标方法
Object result = pjd.proceed();
// 打印返回结果
try {
String s = JSON.toJSONString(result);
log.info("{};.{};()【方法返回结果为】:{};", className, methodName, s);
}; catch (Exception e) {
log.info("{};.{};()【方法返回结果打印失败】:{};", className, methodName, e);
};
// 获取执行完的时间
long time = System.currentTimeMillis() - startTime;
log.info("{};.{};()【方法执行时长为】:{};{};", className, methodName, time, " ms");
// 如果使用第一种方式,把这里注释掉
// TODO 这里可以考虑新加一个异步方法,保存信息到数据库,入参,出参,请求人,请求时间,ip信息等,如果有异常,还有异常信息。
if (gmLog != null) {
boolean save = gmLog.isSave();
if (save) {
String val = gmLog.value();
// 调用异步保存数据库方法
int i = this.saveLog(pjd, time, val);
if (i > 0) {
// 判断插入条数,大于0,保存成功。
log.info("{};.{};()【{};】:===================", className, methodName, "保存数据库成功!");
};
};
};
return result;
};
/**
* 是否存在注解,如果存在就获取
* @param joinPoint
* @return
*/
private GmLog getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(GmLog.class);
};
return null;
};
/**
* 保存到数据库
* @param joinPoint
* @param time 方法执行时间 单位ms
* @param val 方法请求描述
* @return
*/
private int saveLog(JoinPoint joinPoint, long time, String val) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
JSONObject jsonObject = new JSONObject();
// ip地址
String hostAddress = "";
try {
hostAddress = InetAddress.getLocalHost().getHostAddress();
}; catch (Exception e) {
log.error("获取ip失败");
};
// redis.getUserId(); 结合实际情况 获取当前登录人信息
// 类名
String className = joinPoint.getTarget().getClass().getName();
// 获取执行的方法名称
String methodName = joinPoint.getSignature().getName();
String url = request.getRequestURL().toString();
String method = request.getMethod();
jsonObject.put("ip", hostAddress);
jsonObject.put("className", className);
jsonObject.put("methodName", methodName);
jsonObject.put("url", url);
// 执行时间
jsonObject.put("time", time);
jsonObject.put("createTime", new Date());
jsonObject.put("createUser", "GMaya");
// 操作描述
jsonObject.put("operation", val);
jsonObject.put("method", method);
String s = jsonObject.toJSONString();
// 调用日志service的add方法即可!
log.info(s);
return 1;
};
};
controller层接口展示:
//只需要添加注解即可,以及是否保存到数据库,默认不保存。
@RequestMapping("add")
@GmLog(value = "新增登录信息" , isSave = true)
public R add(@RequestBody F<UserEntity> f) {
// 登录用户信息
UserEntity user = this.getUser(f.getToken());
return R.data(userService.add(f,user));
};
36.spring循环依赖
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。singletonObjects存放的是已经初始化好了的Bean
第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完。earlySingletonObjects存放的是实例化了,但是未初始化的Bean
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂。singletonFactories存放的是FactoryBean。假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean