0%

javaSpring学习(二)——SpringCore

容器

容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。

通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接,解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。

Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。

ioc和aop

IoC 解决了以下问题:

  • 创建了许多重复对象,造成大量资源浪费;
  • 更换实现类需要改动多个地方;
  • 创建和配置组件工作繁杂,给组件调用方带来极大不便。

AOP 解决了以下问题:

  • 切面逻辑编写繁琐,有多少个业务方法就需要编写多少次。

Spring IOC

IoC(Inverse of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。

IOC的思想是:IoC的核心思想在于资源统一管理,你所持有的资源全部放入到IoC容器中,而你也只需要依赖IoC容器,该容器会自动为你装配所需要的具体依赖,而不是传统的在你的对象内部直接控制。对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。

  • 谁控制谁,控制什么:IoC 容器控制了对象主要控制了外部资源获取(不只是对象包括比如文件等)
  • 为何是反转,哪些方面反转了:传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在 传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者 实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。

注入内容包括:注入某个对象所需要的外部资源(包括对象、资源、常量数据)

为什么叫控制反转?

Spring所倡导的开发方式:所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

  • 控制 :指的是对象创建(实例化、管理)的权力
  • 反转 :控制权交给外部环境(Spring 框架、IoC 容器)

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

  IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

IOC/DI

引言:Spring 能有效地组织J2EE应用各层的对象。不管是控制层的Action对象,还是业务层的Service对象,还是持久层的DAO对象,都可在Spring的 管理下有机地协调、运行。Spring将各层的对象以松耦合的方式组织在一起,Action对象无须关心Service对象的具体实现,Service对 象无须关心持久层对象的具体实现,各层对象的调用完全面向接口。当系统需要重构时,代码的改写量将大大减少。

上面所说的一切都得宜于Spring的核心机制,依赖注入。依赖注入让bean与bean之间以配置文件组织在一起,而不是以硬编码的方式耦合在一起。

IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的

如果调用者使用到被调用对象才会从spring容器中取出依赖的对象注入到使用的类中,如果不用则会放回spring容器的对象池中,做到内存节省并且代码的耦合度也降低。面向接口编程中,让依赖注入只需要找到符合规范的接口注入即可实现调用者和被调用者解耦。对象的调用关系由spring管理。

spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对pojo之间依赖关系的管理

  IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

构造器依赖注入和 Setter方法注入的区别

构造函数注入 setter 注入
没有部分注入 有部分注入
不会覆盖 setter 属性 会覆盖 setter 属性
任意修改都会创建一个新实例 任意修改不会创建一个新实例
适用于设置很多属性 适用于设置少量属性

两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。

装配bean

  1. Spring的bean容器也叫beanfactory,我们常用的applicationcontext实际上内部有一个listablebeanfactory实际存储bean的map。
  2. bean加载过程:spring容器加载时先读取配置文件,一般是xml,然后解析xml,找到其中所有bean,依次解析,然后生成每个bean的beandefinition,存在一个map中,根据beanid映射实际bean的map。
  3. bean初始化:加载完以后,如果不启用懒加载模式,则默认使用单例加载,在注册完bean以后,可以获取到beandefinition信息,然后根据该信息首先先检查依赖关系,如果依赖其他bean则先加载其他bean,然后通过反射的方式即newinstance创建一个单例bean。

为什么要用反射呢,因为实现类可以通过配置改变,但接口是一致的,使用反射可以避免实现类改变时无法自动进行实例化。

当然,bean也可以使用原型方式加载,使用原型的话,每次创建bean都会是全新的。

第一步:

完成类的功能;例如MailService和一个UserService,实现用户注册和登录;

1
2
3
4
5
6
7
8
public class UserService {
private MailService mailService;

public void setMailService(MailService mailService) {
this.mailService = mailService;
}
···
}

第二步:

编写一个特定的application.xml配置文件,告诉Spring的IoC容器应该如何创建并组装Bean:

1
2
3
4
5
6
7
8
9
10
11
12
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="userService" class="com.itranswarp.learnjava.service.UserService">
<property name="mailService" ref="mailService" />
</bean>

<bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>

注意观察上述配置文件,其中与XML Schema相关的部分格式是固定的,我们只关注两个<bean ...>的配置:

  • 每个<bean ...>都有一个id标识,相当于Bean的唯一ID;
  • userServiceBean中,通过<property name="..." ref="..." />注入了另一个Bean;
  • Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。

把上述XML配置文件用Java代码写出来,就像这样:

1
2
3
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);

只不过Spring容器是通过读取XML文件后使用反射完成的。

如果注入的不是Bean,而是booleanintString这样的数据类型,则通过value注入

1
2
3
4
5
6
7
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="password" />
<property name="maximumPoolSize" value="10" />
<property name="autoCommit" value="true" />
</bean>

第三步:

创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码:

1
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

接下来,我们就可以从Spring容器中“取出”装配好的Bean然后使用它:

1
2
3
4
// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");

完整的main()方法如下:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
UserService userService = context.getBean(UserService.class);
User user = userService.login("bob@example.com", "password");
System.out.println(user.getName());
}
}

ApplicationContext

Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;

通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext

持有IoC容器后,通过getBean()方法获取Bean的引用。

我们从创建Spring容器的代码:

1
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

可以看到,Spring容器就是ApplicationContext,它是一个接口,有很多实现类,这里我们选择ClassPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。

获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中我们可以根据Bean的ID获取Bean,但更多的时候我们根据Bean的类型获取Bean的引用:

1
UserService userService = context.getBean(UserService.class);

Spring还提供另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似:

1
2
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);

BeanFactoryApplicationContext的区别在于,BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。实际上,ApplicationContext接口是从BeanFactory接口继承而来的,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext,很少会考虑使用BeanFactory

使用Annotation配置

使用XML配置的优点是所有的Bean都能一目了然地列出来,并通过配置注入能直观地看到每个Bean的依赖。它的缺点是写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中。

使用Annotation配置,可以完全不需要XML,让Spring自动扫描Bean并组装它们。

相关阅读

Spring AOP

AOP(Aspect-Oriented Programming:面向切面编程)利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了 多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

AOP中的相关概念

  • Advice(通知):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。

    【就是你想要的功能,也就是上面说的 安全,事物,日志等。你给先定义好,然后在想用的地方用一下。】

  • Joint point(连接点):spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

    【你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。】

  • Aspect(切面): 切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before,after,around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

  • introduction(引入):允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

  • Target(目标对象):引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

  • Weaving(织入):把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring采用的是运行时,为什么是运行时,后面解释。

      关键就是:切点定义了哪些连接点会得到通知

  • proxy(代理):怎么实现整套aop机制的,都是通过代理。

AOP原理理解

​ spring用代理类包裹切面,把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标bean。

  现在可以自己想一想,怎么搞出来这个伪装类,才不会被调用者发现(过JVM的检查,JAVA是强类型检查,哪里都要检查类型)。

  1.实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。

  顺着这个思路想,要是本身这个类就没实现一个接口呢,你怎么伪装我,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用第2种代理方式,创建一个目标类的子类,生个儿子,让儿子伪装我

  2.生成子类调用,这次用子类来做为伪装类,当然这样也能逃过JVM的强类型检查,我继承的吗,当然查不出来了,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事物等)。

  这次的对比就是,儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。

  前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。

  后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。

  相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。

Spring实现AOP——动态代理

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了(为啥?你写一个JDK Proxy的demo就知道了),这时候Spring AOP会使用Cglib,生成一个被代理对象的子类,来作为代理.

  1. JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理;其核心的两个类是InvocationHandler和Proxy。
  2. CGLIB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM(开源的java字节码编辑类库)操作字节码实现的,性能比JDK强;需要引入包asm.jar和cglib.jar。

如下图所示:

img

AOP:

AOP的切面,切点,增强器一般也是配置在xml文件中的,所以bean容器在解析xml时会找到这些内容,并且首先创建增强器bean的实例。

基于上面创建bean的过程,AOP起到了什么作用呢,或者是是否有参与到其中呢,答案是有的。

在获得beandefinition的时候,spring容器会检查该bean是否有aop切面所修饰,是否有能够匹配切点表达式的方法,如果有的话,在创建bean之前,会将bean重新封装成一个动态代理的对象。

代理类会为bean增加切面中配置的advisor增强器,然后返回bean的时候实际上返回的是一个动态代理对象。

所以我们在调用bean的方法时,会自动织入切面的增强器,当然,动态代理既可以选择jdk增强器,也可以选择cglib增强器。

Spring事务:

spring事务其实是一种特殊的aop方式。在spring配置文件中配置好事务管理器和声明式事务注解后,就可以使用@transactional进行事务方法的处理了。

事务管理器的bean中会配置基本的信息,然后需要配置事务的增强器,不同方法使用不同的增强器。当然如果使用注解的话就不用这么麻烦了。

然后和aop的动态代理方式类似,当Spring容器为bean生成代理时,会注入事务的增强器,其中实际上实现了事务中的begin和commit,所以执行方法的过程实际上就是在事务中进行的。

AOP使用场景

  1. Authentication 权限检查
  2. Caching 缓存
  3. Context passing 内容传递
  4. Error handling 错误处理
  5. Lazy loading 延迟加载
  6. Debugging  调试
  7. logging, tracing, profiling and monitoring 日志记录,跟踪,优化,校准
  8. Performance optimization 性能优化,效率检查
  9. Persistence  持久化
  10. Resource pooling 资源池
  11. Synchronization 同步
  12. Transactions 事务管理

过滤器filter、拦截器interceptor、和AOP的区别与联系

filter过滤器

  • 过滤器拦截web访问url地址。 严格意义上讲,filter只是适用于web中,依赖于Servlet容器,利用Java的回调机制进行实现。
  • Filter过滤器:和框架无关,可以控制最初的http请求,但是更细一点的类和方法控制不了。
  • **过滤器可以拦截到方法的请求和响应(ServletRequest request, ServletResponse response)**,并对请求响应做出像响应的过滤操作,
  • 比如设置字符编码,鉴权操作

Interceptor拦截器

  • 拦截器拦截以 .action结尾的url,拦截Action的访问。 Interfactor是基于Java的反射机制(APO思想)进行实现,不依赖Servlet容器。
  • 拦截器可以在方法执行之前(preHandle)和方法执行之后(afterCompletion)进行操作,回调操作(postHandle)可以获取执行的方法的名称,请求(HttpServletRequest)
  • Interceptor:可以控制请求的控制器和方法,但控制不了请求方法里的参数(只能获取参数的名称,不能获取到参数的值)
  • 用于处理页面提交的请求响应并进行处理,例如做国际化,做主题更换,过滤等)。

Spring AOP拦截器

  • 只能拦截Spring管理Bean的访问(业务层Service)。 具体AOP详情参照 Spring AOP:原理、 通知、连接点、切点、切面、表达式
  • 实际开发中,AOP常和事务结合:Spring的事务管理:声明式事务管理(切面)
  • AOP操作可以对操作进行横向的拦截,最大的优势在于他可**以获取执行方法的参数( ProceedingJoinPoint.getArgs() )**,对方法进行统一的处理。
  • Aspect : 可以自定义切入的点,有方法的参数,但是拿不到http请求,可以通过其他方式如RequestContextHolder获得( ServletRequestAttributes servletRequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); )。
  • 常见**使用日志,事务,请求参数安全验证

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

参考文章