IoC
-
Inversion of Control,控制反转。
-
Spring核心容器的主要组件是Bean工厂(BeanFactory),Bean工厂使用控制反转(IoC)模式来降低程序代码之间的耦合度,并提供了面向切面编程(AOP)的实现。
-
控制反转,就是将设计好的对象交给容器控制。创建对象的控制权,被反转到了Spring框架上。
通常,我们实例化一个对象时,都是使用类的构造方法来new一个对象,这个过程是由我们自己来控制的,而控制反转就把new对象的工交给了Spring容器。
-
IoC的主要实现方式有两种:依赖查找、依赖注入。
-
依赖注入(Dependency Injection):一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
组件不做定位查询,只提供标准的Java方法让容器去决定依赖关系。容器全权负责组件的装配,把符合依赖关系的对象通过Java Bean属性或构造方法传递给需要的对象。
由IoC容器动态地将某个对象所需要的外部资源(包括对象、资源、常量数据)注入到组件(Controller, Service等)之中。 -
依赖查找:主要是容器为组件提供一个回调接口和上下文环境。这样一来,组件就必须自己使用容器提供的API来查找资源和协作对象,控制反转仅体现在那些回调方法上,容器调用这些回调方法,从而应用代码获取到资源。
-
Spring依赖注入的方式主要有四个,基于注解注入方式、set注入方式、构造器注入方式、静态工厂注入方式。推荐使用基于注解注入方式,配置较少,比较方便。
实现原理
-
使用的技术:1、xml配置文件;2、dom4j解析XML文件;3、工厂设计模式;4、反射
xml文件的作用:一是配置信息,二是存储信息。来让容器知道需要创建的对象与对象的关系。
服务器得到这些配置信息要通过解析工具,dom4j就是这样的一个解析框架,它可以修改其中的文件。dom4j是树形结构,通过节点来解析。
当web容器启动的时候,spring的全局bean的管理器会去xml配置文件中扫描的包下面获取到所有的类,并根据使用的注解,进行对应的封装,封装到全局的bean容器中进行管理,一旦容器初始化完毕,beanID以及bean实例化的类对象信息就全部存在了。
现在当在某个service里面调用另一个bean的某个方法的时候,只需要依赖注入进来另一个bean的Id即可,调用的时候,spring会去初始化完成的bean容器中获取,如果存在就把依赖的bean的类的实例化对象返回,然后就可以调用依赖的bean的相关方法或属性等;
- 工厂模式:工厂模式可将Java对象的调用者从被调用者的实现逻辑中分离出来。属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
简单的说就是不用自己new对象了,对象的实例化都交给工厂来完成,需要对象的时候直接问工厂拿一个就行。spring IOC与工厂模式并不是完全相同的,最大的不同在于普通的工厂模式内部还是使用new来创建对象,但是spring IOC是用反射来创建对象(更大的灵活性)。 - Spring Bean的创建是典型的工厂模式。(spring将bean创建好放到容器中,等到需要的时候将实例化对象返回)。一系列的Bean工厂,IOC容器为开发者管理对象间的依赖关系提供了很多便利和基础服务。
【转载】Spring IOC与工厂模式
- 写一个bean工厂模拟spring ioc:
//文件名:bean.properties;spring中用的是XML,这里为了简化就用properties,原理都是一样的: circle=com.demo.Circle rectangle=com.demo.Rectangle square=com.demo.Square //文件名:BeanFactory.java package com.demo; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; public class BeanFactory { //配置对象(类比spring IOC容器中的Bean定义注册表) private static final Properties props; //保存创建好的对象的容器,与类名组成key-value对(类比spring IOC容器中的Bean缓存池) private static Map<String, Object> beans; static { props = new Properties(); //通过类加载器读入配置文件 InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"); try { //加载配置文件到配置对象 props.load(in); //初始化容器 beans = new HashMap<>(); Enumeration<Object> keys = props.keys(); //循环遍历配置对象中的所有的类名(key) while (keys.hasMoreElements()){ String key = keys.nextElement().toString(); //通过类名拿到全类名(value) String beanPath = props.getProperty(key); //利用全类名反射创建对象 Object value = Class.forName(beanPath).getDeclaredConstructor().newInstance(); //将对象放入容器中 beans.put(key, value); } } catch (IOException e) { throw new ExceptionInInitializerError("初始化properties失败!程序终止!"); } catch (Exception e){ e.printStackTrace(); } } public static Object getBean(String beanName){ //从容器中获取对象 return beans.get(beanName); } }
- Spring IOC容器接口继承图,BeanFactory是简单容器,他实现了容器的基本功能,典型方法如 getBean、containsBean等。ApplicationContext是应用上下文,他在简单容器的基础上,增加上下文的特性。我们开发时一般都是使用ApplicationContext接口,因为他的功能比BeanFactory更强大。ApplicationContext有好多的实现类,最常用的是——ClassPathXmlApplicationContext。
AOP
- Aspect-Oriented Programming
- 面向切面编程(AOP)将程序运行过程分解成各个切面。
AOP从程序运行角度考虑程序的结构,提取业务处理过程的切面。 - 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 (横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等))
- 将日志记录,性能统计,安全控制,事务处理,异常处理,权限控制,参数校验等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
- 让关注点代码(重复代码)与业务代码分离(对很多功能都有的重复代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。)
实现原理
- 通过动态代理。由 JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术)实现。动态代理就是在不修改原有类对象方法的源代码基础上,通过代理对象实现原有类对象方法的增强,也就是拓展原有类对象的功能。(比如业务A和业务B现在需要一个相同的操作,传统方法我们可能需要在A、B中都加入相关操作代码,而应用AOP就可以只写一遍代码,A、B共用这段代码。并且,当A、B需要增加新的操作时,可以在不改动原代码的情况下,灵活添加新的业务逻辑实现。)
JDK动态代理:只针对接口操作。
CGLIB:可以针对没有接口的java类和有接口的java类。
-
切面(Aspect):共有功能的实现。如日志切面、权限切面、验签切面等。在实际开发中通常是一个存放共有功能实现的标准Java类。当Java类使用了@Aspect注解修饰时,就能被AOP容器识别为切面。
-
通知(Advice):切面的具体实现。就是要给目标对象织入的事情。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际开发中通常是切面类中的一个方法,具体属于哪类通知,通过方法上的注解区分。
前置通知:在某连接点之前执行的通知
后置通知:在一个匹配的方法返回的时候执行。 -
连接点(JoinPoint):程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。
-
切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。
execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
修饰符匹配 modifier-pattern? 例:public private
返回值匹配 ret-type-pattern 可以用 * 表示任意返回值
类路径匹配 declaring-type-pattern? 全路径的类名
方法名匹配 name-pattern 可以指定方法名或者用 * 表示所有方法;set* 表示所有以set开头的方法
参数匹配 (param-pattern) 可以指定具体的参数类型,多个参数用“,”分隔;可以用 * 表示匹配任意类 型的参数;可以用 (…) 表示零个或多个任意参数
异常类型匹配throws-pattern? 例:throws Exception比如,在上面所说的连接点的基础上,来定义切入点。我们有一个类,类里有10个方法,那就产生了几十个连接点。但是我们并不想在所有方法上都织入通知,我们只想让其中的几个方法,在调用之前检验下入参是否合法,那么就用切点来定义这几个方法,让切点来筛选连接点,选中我们想要的方法。切入点就是来定义哪些类里面的哪些方法会得到通知。
-
目标对象(Target):那些即将切入切面的对象,也就是那些被通知的对象。这些对象专注业务本身的逻辑,所有的共有功能等待AOP容器的切入。
-
代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。目标对象被织入共有功能后产生的对象。
-
织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring是在运行时完成织入,运行时织入通过Java语言的反射机制与动态代理机制来动态实现。
使用场景举例
-
一个简单的切面类
@Aspect //该注解被AOP容器识别为切面 @Component public class SignAop { //切入点表达式:定义切面需要切入的位置,范围是controller包下所有的类的所有方法 @Pointcut("execution(public * cn.wbnull.springbootdemo.controller.*.*(..))") private void signAop() { } @Before("signAop()") //前置通知 public void doBefore(JoinPoint joinPoint) throws Exception { //code } @AfterReturning(value = "signAop()", returning = "params") //后置通知 public JSONObject doAfterReturning(JoinPoint joinPoint, JSONObject params) { //code } }
-
异常处理。(SpringBoot开发详解-- 异常统一管理以及AOP的使用)
Spring全局异常处理
使用 @ControllerAdvice ,将所有捕获的异常统一返回CommonResponse。到这里还没有用到AOP。接下来:使用接口若出现了异常,很难知道是谁调用接口,是前端还是后端出现的问题导致异常的出现,这时候AOP就发挥作用了,定义一个切面类:
@Aspect @Component public class HttpAspect { private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class); @Autowired private ExceptionHandle exceptionHandle; @Pointcut("execution(public * com.zzp.controller.*.*(..))") public void log(){ } @Before("log()") public void doBefore(JoinPoint joinPoint){ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //url LOGGER.info("url={}",request.getRequestURL()); //method LOGGER.info("method={}",request.getMethod()); //ip LOGGER.info("id={}",request.getRemoteAddr()); //class_method LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName()); //args[] LOGGER.info("args={}",joinPoint.getArgs()); } @Around("log()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Result result = null; try { } catch (Exception e) { return exceptionHandle.exceptionGet(e); } if(result == null){ return proceedingJoinPoint.proceed(); }else { return result; } } @AfterReturning(pointcut = "log()",returning = "object")//打印输出结果 public void doAfterReturing(Object object){ LOGGER.info("response={}",object.toString()); } }
使用@Aspect来声明这是一个切面,使用@Pointcut来定义切面所需要切入的位置,这里是对每一个HTTP请求都需要切入。
在进入方法之前使用@Before记录了调用的接口URL,调用的方法,调用方的IP地址以及输入的参数等。
在整个接口代码运作期间,使用@Around来捕获异常信息,并用之前定义好的Result进行异常的返回,最后使用@AfterReturning来记录出參。
【参考文档】
SpringAOP原理分析
SpringIoc 实现原理
深入理解Spring两大特性:IoC和AOP
Spring IOC与工厂模式
SpringBoot开发详解-- 异常统一管理以及AOP的使用