目录
3.7 DI——Dependency Injection(依赖注入)
一、IOC概述
1.1 什么是IOC
IoC不是什么技术,它是一种设计模式。
它还有一个名字叫做依赖注入(Dependency Injection)。
控制反转(Inversion of Control)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。
从对象调用者角度又叫做依赖注入,即Dependency Injection(DI),通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的容器,将其所依赖的对象的引用传递给它,也可以说,依赖被注入到对象中,这个容器就是我们经常说到IOC容器。
Sping及SpringBoot框架的核心就是提供了一个基于注解实现的IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
传统与IOC对象依赖的实现方式如下:
// 传统方式创建对象及其依赖对象
public class TraditionalApplication {
private ExampleDependency dependency = new ExampleDependency();
public void doSomething() {
dependency.performTask();
}
}
// 使用IOC容器后的对象声明方式
public class IoCApplication {
private ExampleDependency dependency;
// 依赖注入
public IoCApplication(ExampleDependency dependency) {
this.dependency = dependency;
}
public void doSomething() {
dependency.performTask();
}
}
1.2 Spring IOC
Spring框架是Java企业级应用开发中的重量级选手,其中IOC(控制反转)是其核心功能之一。通过引入IOC容器,Spring改变了对象的创建方式及其依赖关系的管理,进而带来了应用程序设计的革命。
控制反转是一种设计原理,用于减少计算机代码之间的耦合度。在没有IOC的传统程序设计中,对象的创建以及它们之间的依赖关系通常是由开发者在代码内部显式地定义。而在使用IOC之后,这些工作被反转了:不再由对象自身控制依赖对象的创建和管理,而是交由外部容器来处理,对象只是声明它所需要的资源或依赖。
IOC容器的初始化和依赖注入过程
1.2.1 IOC容器初始化过程
(1)资源定位:
这个过程使用,ResourceLoader进行,所谓资源就是定义了Bean的xml文件。
spring定义了多种资源存放的方式, 如classpath、文件系统等。
FileSystemResource、ClassPathResource、ServletContextResoruce。
用户可以根据需选择合适的资源实现类访问资源。我们一般都使用类路径或文件系统定位资源。
(2)Bean解析和注册:
找到了定义bean资源位置,下一步就就是讲他们载入到系统中。
IOC将xml中定义的bean读入成为Document对象,并将Document对象解析成为BeanDefinition对象。spring将BeanDefinition对象存放中HashMap中,Ioc容器载入并解析完bean后,将BeanDefinition通过存放在了BeanFactory的beanDefinitionMap中。
至此完成了容器的初始化过程。
1.2.2 IOC容器依赖注入过程
容器完成初始化,并没有完成依赖注入,或者说大部分bean实例并没有在这个时候创建出来,也没有注入到需要使用它们的bean中,依赖注入是发生在第一次调用getBean方法时(除了lazy-init方式)。
(1)创建bean:
在SimpleInstantiationStrategy中,通过JDK的反射创建了bean实例,也可以通过CGLIB创建bean实例。
(2)注入属性:
通过BeanWrapper对bean的属性进行注入。通过BeanDefinition中对bean的定义信息进行递归,注入的是bean,就调用getBean创建和注入bean;注入的是属性,就直接注入属性。
1.3 Spring IOC 初始化
通过以上学习,我们了解到IOC容器的主要作用是创建对象、维护生命周期、配置依赖关系。
容器负责非侵入式地管理应用中的对象,简化了编程模型并提高了组件的可重用性。
在Spring中,BeanFactory和ApplicationContext是实现IOC容器的方式。
1,BeanFactory:是最简单的容器,提供基本的依赖注入支持。
BeanFactory处理Bean的定义,并用于创建和管理Bean生命周期。
它用以下方式来实现:
Bean的配置: 支持多种配置方式,如XML、注解、Java配置类。
延迟加载: BeanFactory创建的bean默认不是在启动时就全部加载,
而是在要求的时候才创建,从而减少资源消耗。
Bean的生命周期由BeanFactory处理,这包括:
实例化: 根据Bean的定义创建一个对象。
填充属性: 根据Bean的定义,注入所有的必需的属性。
调用Bean的初始化方法: 如Bean实现了InitializingBean接口,或通过配置指定了init方法。
Bean的使用: 现在Bean已准备好被应用使用。
销毁: 当容器关闭时,BeanFactory会销毁Singleton类型的Beans。
2,ApplicationContext:在BeanFactory基础上构建,提供了更多的企业级功能,如事件发布、国际化信息支持等。
ApplicationContext是BeanFactory的子接口,提供了更多面向实际开发的高级特性。
虽然ApplicationContext扩展了BeanFactory,但它额外提供如下功能:
立即加载: ApplicationContext会在启动的时候预先创建并配置所有单例bean。
国际化支持: 提供了消息国际化的方法。
事件发布: 支持事件的发布及监听。
单例Bean和多实例Bean的生命周期主要区别在于实例化和销毁的管理方式,单例Bean在容器启动时创建一个实例,并由容器负责管理其生命周期的完整过程。而多实例Bean在每次请求时创建新的实例,并且销毁过程需要开发者手动管理。
IOC容器不仅仅是技术实现,更是一种编程哲学,它鼓励开发者关注业务逻辑的实现,而不是耗费精力在对象的创建和管理上。这种解耦让开发者能够编写更清晰、更灵活、更易于测试的代码。
简易实现原理如下:
第一步,IOC实现流程
第二步,构建BeanDefinition(XML解析保存元数据)
package com.tiny.spring.beans.factory.config;
/**
* @Description: Bean配置元信息
*/
public class BeanDefinition {
private String id;
private String className;
public BeanDefinition() {
}
public BeanDefinition(String id, String className) {
this.id = id;
this.className = className;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
}
第三步,构建ClassPathXmlApplicationContext
package com.tiny.spring.context.support;
import com.tiny.spring.beans.factory.config.BeanDefinition;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author: markus
* @date: 2023/10/7 8:16 PM
* @Description: 基于xml的Spring应用上下文
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class ClassPathXmlApplicationContext {
private List<BeanDefinition> beanDefinitions = new ArrayList<>();
private Map<String, Object> singletons = new HashMap<>();
public ClassPathXmlApplicationContext(String pathname) {
this.readXml(pathname);
this.instanceBeans();
}
private void readXml(String pathname) {
SAXReader saxReader = new SAXReader();
try {
URL xmlPath = this.getClass().getClassLoader().getResource(pathname);
Document document = saxReader.read(xmlPath);
Element rootElement = document.getRootElement();
// 对配置文件的每一个<bean>标签进行处理
for (Element element : rootElement.elements()) {
// 获取Bean的基本信息
String beanId = element.attributeValue("id");
String beanClassName = element.attributeValue("class");
BeanDefinition beanDefinition = new BeanDefinition(beanId, beanClassName);
// 将Bean的定义存放到BeanDefinition
beanDefinitions.add(beanDefinition);
}
} catch (DocumentException e) {
e.printStackTrace();
}
}
/**
* 利用反射创建Bean实例,并存储在singletons中
*/
private void instanceBeans() {
for (BeanDefinition beanDefinition : beanDefinitions) {
try {
singletons.put(beanDefinition.getId(), Class.forName(beanDefinition.getClassName()).newInstance());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* 对外提供的方法,让外部程序获取Bean实例
* @param beanName
* @return
*/
public Object getBean(String beanName) {
return singletons.get(beanName);
}
}
第四步,测试
package com.tiny.spring.test;
import com.tiny.spring.beans.BeansException;
import com.tiny.spring.context.support.ClassPathXmlApplicationContext;
import com.tiny.spring.test.service.AService;
/**
* @author: markus
* @date: 2023/10/7 8:37 PM
* @Description: 最原始的IoC容器功能测试
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class OriginalIoCContainerTest {
public static void main(String[] args) throws BeansException {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("beans.xml");
AService aService = (AService) classPathXmlApplicationContext.getBean("aService");
aService.sayHello();
}
}
通过本例我们可以知道,IOC的BeanFactory其实就是解析XML,然后通过XML生成相应的Bean,通过Map存储Bean信息。
其实到SpringBoot,使用约定大于配置,就是把通过XML换成注解形式实现,实例化相关对象。
依赖注入其实就是扫描相关BeanFactory Map的数据,然后通过制定的注解给相关bean进行熟悉赋值。
1.4 Spring IOC 依赖注入
依赖注入是IOC的一种实现方式,它指的是将一个对象的依赖关系(如构造函数、方法参数等)从对象本身分离出来,由外部容器动态地注入这些依赖关系。
DI通过接口或配置来实现,使得不同模块之间的依赖关系更清晰,并且能够方便地进行替换和扩展。
注入属性-外部Bean
<!--<1、将service和da对象进行创建>-->
<bean id="userService" class="com.study.service.UserService">
<!--<注入userDao对象>-->
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.study.dao.UserDaoImpl"></bean>
注入属性-内部Bean
最常见的使用内部bean的情况为一对多的情况
<!--<内部bean>-->
<bean id="emp" class="com.study.bean.Emp">
<!--<设置两个普通属性>-->
<property name="eName" value="lucy"></property>
<property name="gender" value="女"></property>
<!--<设置对象类型属性>-->
<property name="dept">
<bean id="dept" class="com.study.bean.Dept">
<property name="dName" value="安保部"></property>
</bean>
</property>
</bean>
关键点:
在类前面加注解:@Component,在需要注入的类里面加注解:@Autowired,
这样xml里面的自动扫描就会扫描到这些加了注解的类和属性,
在实例化bean的时候,Spring容器会把加了@Component的类实例化;
在实际运行时,会给加了@Autowired的属性注入对应的实例。
Spring中的自动装配有哪些限制?
1,如果使用了构造器注入或者setter注入,那么将覆盖自动装配的依赖关系。
2,基本数据类型的值、字符串字面量、类字面量无法使用自动装配来注入。
3,有先考虑使用显式的装配来进行更精确的依赖注入而不是使用自动装配。
和自动装配相关的注解有哪些?
@Required:该依赖关系必须装配(手动或自动装配),否则将抛出BeanInitializationException异常。
@Autowired:自动装配,默认按类型进行自动装配。
@Qualifier:如果按类型自动装配时有不止一个匹配的类型,那么可以使用该注解指定名字来消除歧义。
一个简单的实现原理如下:
package com.hand.bean;
import com.hand.MyLog;
import com.hand.anno.Bean;
import com.hand.anno.Di;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class AnnoApplicationContext implements ApplicationContext {
private String rootPath = null;
private Map<Class, Object> beanFactory = new HashMap<>();
@Override
public Object getBean(Class clazz) {
return beanFactory.get(clazz);
}
public AnnoApplicationContext(String packageStr) {
try {
String packagePath = packageStr.replaceAll("\\.", "\\\\");
Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packagePath);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String filePath = URLDecoder.decode(url.getFile(), "utf-8");
// MyLog.logger.info(filePath);
rootPath = filePath.substring(0, filePath.length() - packagePath.length());
loadBean(new File(filePath));
}
} catch (Exception e) {
e.printStackTrace();
}
loadDi();
}
/**
* 对象加入容器
*/
private void loadBean(File file) throws Exception {
if (file.isDirectory()) {
File[] childFiles = file.listFiles();
if (childFiles == null || childFiles.length == 0) {
return;
}
for (File childFile : childFiles) {
if (childFile.isDirectory()) {
loadBean(childFile);
continue;
}
String pathWhitClass = childFile.getAbsolutePath().substring(rootPath.length() - 1);
if (!pathWhitClass.contains(".class")) {
continue;
}
String allName = pathWhitClass.replaceAll("\\\\", ".").replace(".class", "");
MyLog.logger.info("类:" + allName);
Class<?> clazz = Class.forName(allName);
if (clazz.isInterface()) {
continue;
}
Bean bean = clazz.getAnnotation(Bean.class);
if (bean != null) {
Object instance = clazz.getConstructor().newInstance();
if (clazz.getInterfaces().length > 0) {
beanFactory.put(clazz.getInterfaces()[0], instance);
} else {
beanFactory.put(clazz, instance);
}
}
}
}
}
/**
* 依赖注入
*/
private void loadDi() {
for (Object obj : beanFactory.values()) {
Class<?> clazz = obj.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
Di di = field.getAnnotation(Di.class);
if (di == null) {
continue;
}
field.setAccessible(true);
try {
field.set(obj, beanFactory.get(field.getType()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
1.5 SpringBoot IOC 零配置
在SpringBoot中新增一个用于替代原xml配置文件的ApplicationCfg类。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 容器的配置类
*/
@Configuration
@ComponentScan(basePackages="com.zhangguo.Spring051.ioc06")
public class ApplicationCfg {
@Bean
public User getUser(){
return new User("成功");
}
}
@Configuration相当于配置文件中的<beans/>,
@ComponentScan相当于配置文件中的context:component-scan,属性也一样设置
@Bean相当于<bean/>,只能注解在方法和注解上,
一般在方法上使用,源码中描述:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}),方法名相当于id。
启动测试类
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
@org.junit.Test
public void testStoreBook()
{
//容器,注解配置应用程序容器,Spring通过反射ApplicationCfg.class初始化容器
ApplicationContext ctx=new AnnotationConfigApplicationContext(ApplicationCfg.class);
BookService bookservice=ctx.getBean(BookService.class);
bookservice.storeBook("《Spring MVC权威指南 第四版》");
User user1=ctx.getBean("user1",User.class);
user1.show();
User getUser=ctx.getBean("getUser",User.class);
getUser.show();
}
}
容器的初始化通过一个类型完成,Spring通过反射ApplicationCfg.class初始化容器,
中间user1与getUser是否为相同的Bean呢?
答案是否定的,因为在ApplicationCfg中声明的方法getUser当相于在xml文件中定义了一个<bean id="getUser" class="..."/>,在User类上注解@Component("user1")相当于另一个<bean id="user1" class="..."/>
使用零配置和注解虽然方便,不需要编写麻烦的xml文件,但并非为了取代xml,应该根据实例需要选择,或二者结合使用,毕竟使用一个类作为容器的配置信息是硬编码的,不好在发布后修改。
二、SpringBoot IOC 实现
2.1 SpringBoot 概述
Sping及SpringBoot框架的核心就是提供了一个基于注解实现的IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
道生一,一生二,二生三。框架也是一样,基本原理是很简单的,但是被各种复杂的约束,导致复杂。
我们自己动手实现一个基于注解的简单IOC容器,当然由于是个人实现不会真的完全按照SpringBoot框架的设计模式,也不会考虑过多的如循环依赖、线程安全等其他复杂问题, 整个实现原理很简单,扫描注解,通过反射创建出我们所需要的bean实例,再将这些bean放到集合中,对外通过IOC容器类提供一个getBean()方法,用来获取ean实例,废话不多说,下面开始具体设计与实现。
2.2 自定义注解
@Retention(RetentionPolicy.RUNTIME)
public @interface SproutComponet {
String value() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SproutRoute {
RouteEnum value();
}
2.3 jar包扫描类
根据传入jar包,扫描与缓存jar包下所有指定注解的class<?>类对象
public class ClassScanner {
private static Set<Class<?>> classSet = null;
private static Map<String, Class<?>> componetMap = null;
/**
* 获取指定包名下所有class类
* @param packageName
* @return
* @throws Exception
*/
public static Set<Class<?>> getClasses(String packageName) throws Exception {
if (classSet == null){
classSet = ReflectUtils.getClasses(packageName);
}
return classSet;
}
/**
* 缓存所有指定注解的class<?>类对象
* @param packageName
* @return
* @throws Exception
*/
public static Map<String, Class<?>> getBean(String packageName) throws Exception {
if (componetMap == null) {
Set<Class<?>> clsList = getClasses(packageName);
if (clsList == null || clsList.isEmpty()) {
return componetMap;
}
componetMap = new HashMap<>(16);
for (Class<?> cls : clsList) {
Annotation annotation = cls.getAnnotation(SproutComponet.class);
if (annotation == null) {
continue;
}
SproutComponet sproutComponet = (SproutComponet) annotation;
componetMap.put(sproutComponet.value() == null ? cls.getName() :
sproutComponet.value(), cls);
}
}
return componetMap;
}
}
基于ClassScanner,扫描并缓存加有注解的Method对象,为后面实现方法路由提供支持。
public class RouterScanner {
private String rootPackageName;
private static Map<Object, Method> routes = null;
private List<Method> methods;
private volatile static RouterScanner routerScanner;
/**
* get single Instance
*
* @return
*/
public static RouterScanner getInstance() {
if (routerScanner == null) {
synchronized (RouterScanner.class) {
if (routerScanner == null) {
routerScanner = new RouterScanner();
}
}
}
return routerScanner;
}
private RouterScanner() {}
public String getRootPackageName() {
return rootPackageName;
}
public void setRootPackageName(String rootPackageName) {
this.rootPackageName = rootPackageName;
}
/**
* 根据注解 指定方法 get route method
*
* @param queryStringDecoder
* @return
* @throws Exception
*/
public Method routeMethod(Object key) throws Exception {
if (routes == null) {
routes = new HashMap<>(16);
loadRouteMethods(getRootPackageName());
}
Method method = routes.get(key);
if (method == null) {
throw new Exception();
}
return method;
}
/**
* 加载指定包下Method对象
*
* @param packageName
* @throws Exception
*/
private void loadRouteMethods(String packageName) throws Exception {
Set<Class<?>> classSet = ClassScanner.getClasses(packageName);
for (Class<?> sproutClass : classSet) {
Method[] declaredMethods = sproutClass.getMethods();
for (Method method : declaredMethods) {
SproutRoute annotation = method.getAnnotation(SproutRoute.class);
if (annotation == null) {
continue;
}
routes.put(annotation.value(), method);
}
}
}
}
2.4 BeanFacotry实现
(1)定义BeanFacotry接口
接口必须具备三个基本方法:
init() 初始化注册Bean实例
getBean() 获取Bean实例
release() 卸载Bean实例
具体实现接口如下:
public interface ISproutBeanFactory {
/**
* Register into bean Factory
*
* @param object
*/
void init(Object object);
/**
* Get bean from bean Factory
*
* @param name
* @return
* @throws Exception
*/
Object getBean(String name) throws Exception;
/**
* release all beans
*/
void release();
}
(2)BeanFactory接口实现
BeanFactory接口的具体实现,在BeanFacotry工厂中我们需要一个容器,即beans这个Map集合,在初始化时将所有的需要IOC容器管理的对象实例化并保存到 bean 容器中,当需要使用时只需要从容器中获取即可,解决每次创建一个新的实例都需要反射调用 newInstance() 效率不高的问题。
public class SproutBeanFactory implements ISproutBeanFactory {
/**
* 对象map
*/
private static Map<Object, Object> beans = new HashMap<>(8);
/**
* 对象list
*/
private static List<Method> methods = new ArrayList<>(2);
@Override
public void init(Object object) {
beans.put(object.getClass().getName(), object);
}
@Override
public Object getBean(String name) {
return beans.get(name);
}
public List<Method> getMethods() {
return methods;
}
@Override
public void release() {
beans = null;
}
}
实现bean容器类
IOC容器的入口及顶层实现类,声明bena工厂实例,扫描指定jar包,基于注解获取 Class<?>集合,实例化后注入BeanFacotry对象工厂
public class SproutApplicationContext {
private SproutApplicationContext() {}
private static volatile SproutApplicationContext sproutApplicationContext;
private static ISproutBeanFactory sproutBeanFactory;
public static SproutApplicationContext getInstance() {
if (sproutApplicationContext == null) {
synchronized (SproutApplicationContext.class) {
if (sproutApplicationContext == null) {
sproutApplicationContext = new SproutApplicationContext();
}
}
}
return sproutApplicationContext;
}
/**
* 声明bena工厂实例,扫描指定jar包,加载指定jar包下的实例
*
* @param packageName
* @throws Exception
*/
public void init(String packageName) throws Exception {
//获取到指定注解类的Map
Map<String, Class<?>> sproutBeanMap = ClassScanner.getBean(packageName);
sproutBeanFactory = new SproutBeanFactory();
//注入实例工厂
for (Map.Entry<String, Class<?>> classEntry : sproutBeanMap.entrySet()) {
Object instance = classEntry.getValue().newInstance();
sproutBeanFactory.init(instance);
}
}
/**
* 根据名称获取获取对应实例
*
* @param name
* @return
* @throws Exception
*/
public Object getBean(String name) throws Exception {
return sproutBeanFactory.getBean(name);
}
/**
* release all beans
*/
public void releaseBean() {
sproutBeanFactory.release();
}
}
2.5 实现方法路由
提供方法,接受传入的注解,通过RouterScanner与SproutApplicationContext 获取对应Method对象与Bean实例,调用具体方法,从而实现方法路由功能。
public class RouteMethod {
private volatile static RouteMethod routeMethod;
private final SproutApplicationContext applicationContext =
SproutApplicationContext.getInstance();
public static RouteMethod getInstance() {
if (routeMethod == null) {
synchronized (RouteMethod.class) {
if (routeMethod == null) {
routeMethod = new RouteMethod();
}
}
}
return routeMethod;
}
/**
* 调用方法
* @param method
* @param annotation
* @param args
* @throws Exception
*/
public void invoke(Method method, Object[] args) throws Exception {
if (method == null) {
return;
}
Object bean = applicationContext.getBean(method.getDeclaringClass().getName());
if (args == null) {
method.invoke(bean);
} else {
method.invoke(bean, args);
}
}
/**
* 根据注解调用方法
* @param method
* @param annotation
* @param args
* @throws Exception
*/
public void invoke(RouteEnum routeEnum, Object[] args) throws Exception {
Method method = RouterScanner.getInstance().routeMethod(routeEnum);
if (method == null) {
return;
}
Object bean = applicationContext.getBean(method.getDeclaringClass().getName());
if (args == null) {
method.invoke(bean);
} else {
method.invoke(bean, args);
}
}
}
2.6 具体使用
到这里IOC容器的主要接口与实现类都以基本实现,我们看下具体的使用首先初始化IOC容器,这里根据main方法扫描应用程序所在包下的所有类,把有注解的bean实例注入实例容器。
public void start() {
try {
resolveMainClass();
if(mainClass!=null) {
SproutApplicationContext.getInstance().init(mainClass.getPackage().getName());
}
}catch (Exception e) {
// TODO: handle exception
}
}
/**
* 查询main方法的class类
*
*/
private Class<?> resolveMainClass() {
try {
if(!StringUtils.isEmpty(config().getRootPackageName())) {
mainClass = Class.forName(config().getRootPackageName());
}else {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
mainClass = Class.forName(stackTraceElement.getClassName());
break;
}
}
}
} catch (Exception ex) {
// ignore this ex
}
return mainClass;
}
获取bead实例,并调用方法
/**
* 根据注解调用方法
* @param method
* @param annotation
* @param args
* @throws Exception
*/
public void invoke(RouteEnum routeEnum, Object[] args) throws Exception {
//基于IOC实现的方法路由
Method method = RouterScanner.getInstance().routeMethod(routeEnum);
if (method == null) {
return;
}
// 通过Bean容器直接获取实例
Object bean = applicationContext.getBean(method.getDeclaringClass().getName());
if (args == null) {
method.invoke(bean);
} else {
method.invoke(bean, args);
}
}
三、SpringBoot中的IoC详解
在前⾯讲到,在类上⾯添加 @RestController 和 @Controller 注解,就是把这个对象交给Spring管理,Spring 框架启动时就会加载该类,把对象交给Spring管理,这就是Spring中的IoC思想。Spring对于IoC的实现主要是通过工厂设计模式+反射来实现的,当我们需要某个对象的时候,只需要将创建对象的任务交给容器,在程序中只需要调用(注入)即可。
前⾯我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。那么在Spring程序中,我们该如何通过代码来实现IoC呢?
Spring框架为了更好的服务应用程序,提供了俩类注解来实现将对象集中管理创建:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
3.1 @Controller
使⽤ @Controller 存储 bean 的代码如下所⽰:
@Controller // 将对象存储到 Spring 中
public class UserController {
public void sayHi(){
System.out.println("hi,UserController...");
}
}
如何观察这个对象已经存在Spring容器当中了呢? 我们可以通过启动类中的getBean方法来得到Spring中管理的Bean。
@SpringBootApplication
public class IoCdemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(IoCdemoApplication.class, args);
UserController bean = context.getBean(UserController.class);
bean.sayHi();
}
}
在控制台即可观察到输出:
hi,UserController...
但是一旦将@Controller注释掉,程序就会报错找不到这个Bean,这就说明了使用@Controller确实是可以将该类对象交给Spring进行管理
3.2 @Service
@Service
public class UserService {
public void sayHi(String name) {
System.out.println("Hi," + name);
}
}
还是来获取一下这个对象,看看结果如何
ConfigurableApplicationContext context = SpringApplication.run(IoCdemoApplication.class, args);
UserService bean = context.getBean(UserService.class);
bean.sayHi("CSDN");
在控制台即可观察到输出:
Hi,CSDN
如果注释掉@Service就会报错,因此可以证实@Service可以将类对象交给Spring进行管理
3.3 @Component等注解
后续的三个注解如果进行相同方式的验证都会得到一样的结果,这里就不再赘述
@Repository
public class UserRepository {
public void sayHi() {
System.out.println("Hi, UserRepository~");
}
}
@Component
public class UserComponent {
public void sayHi() {
System.out.println("Hi, UserComponent~");
}
}
@Configuration
public class UserConfiguration {
public void sayHi() {
System.out.println("Hi,UserConfiguration~");
}
}
那么既然这么多注解干的都是同一件事,为什么还非要分出这么多注解呢?
3.4 类注解之间的区别
这个也是和咱们前⾯讲的应⽤分层是呼应的,不同的注解标识着不同的信息,这些注解可以让程序员看到类注解之后,就能直接了解当前类的⽤途。
- @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应
- @Servie:业务逻辑层, 处理具体的业务逻辑
- @Repository:数据访问层,也称为持久层. 负责数据访问操作
- @Configuration:配置层. 处理项⽬中的⼀些配置信息
这和每个省/市都有⾃⼰的⻋牌号是⼀样的。⻋牌号都是唯⼀的,标识⼀个⻋辆的。但是为什么还需要设置不同的⻋牌开头呢?⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样。
这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地.
对于一般的开发我们通过不同的注解去确认它是哪一层的代码,这样更方面上下层进行调用
3.5 类注解之间的联系
细心的朋友可能发现了,上述的注解中少了一个@Component注解,我们不妨打开每个注解的源码看看。
我们会发现上述4个注解中都有@Component注解,这说明它们本⾝就是属于 @Component 的 "⼦类"。@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller、@Service 、@Repository 等, 这些注解被称为 @Component 的衍⽣注解。
@Controller、@Service 和 @Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持久化层),在开发过程中,如果你要在业务逻辑层使⽤ @Component 或 @Service,显然@Service是更好的选择。
诸如@Configuration,见名知意就是用来标记配置相关的,比如我们想用Redis的数据库,在使用Redis之前需要对Redis数据库的密码或者库做一些配置,配置好了之后我们需要将这个配置类交给Spring管理方便我们在别的地方直接访问Redis数据库,这时使用@Configuration就是很好的选择。
⽐如杯⼦有喝⽔杯、刷⽛杯等,但是我们更倾向于在⽇常喝⽔时使⽤⽔杯,洗漱时使⽤刷⽛杯。
3.6 法注解@Bean
类注解是添加到某个类上的, 但是存在两个问题
- 使⽤外部包⾥的类, 没办法添加类注解
- ⼀个类, 需要多个对象, ⽐如多个数据源
比如我们想通过引入第三方的工具类去操作数据库,我们想将这个类交给Spring管理方便我们进行二次开发,但是由于第三方包只能读取不能写入的情况,就会陷入进退俩难的情况。
@Bean 同上述类注解的功能一样,都是将标记的对象交给Spring进行管理,但在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
如下代码所⽰:
@Component
public class BeanConfig {
@Bean
public User user(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
}
我们也可以通过设置name属性给Bean对象进行重命名
@Bean(name = {"u1","user1"})
3.7 DI——Dependency Injection(依赖注入)
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象,在之前程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作。
关于依赖注⼊,Spring也给我们提供了三种⽅式:
- 属性注⼊(Field Injection)
- 构造⽅法注⼊(Constructor Injection)
- Setter 注⼊(Setter Injection)
3.7.1 属性注入
属性注⼊是使⽤ @Autowired 实现的,将 Service 类注⼊到 Controller 类中。
Service 类的实现代码如下:
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void sayHi() {
System.out.println("Hi,UserService");
}
}
Controller 类的实现代码如下:
@Controller
public class UserController {
//注⼊⽅法1: 属性注⼊
@Autowired
private UserService userService;
public void sayHi() {
System.out.println("hi,UserController...");
userService.sayHi();
}
}
3.7.2 构造方法注入
构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:
@Controller
public class UserController2 {
//注⼊⽅法2: 构造⽅法
private UserService userService;
@Autowired
public UserController2(UserService userService) {
this.userService = userService;
}
public void sayHi(){
System.out.println("hi,UserController2...");
userService.sayHi();
}
}
如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;
如果类中有多个构造⽅法,那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法。
3.7.3 Setter注入
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解 ,如下代码所⽰:
@Controller
public class UserController3 {
//注⼊⽅法3: Setter⽅法注⼊
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi(){
System.out.println("hi,UserController3...");
userService.sayHi();
}
}
3.7.4 三种注入的优缺点
属性注⼊
优点:
- 简洁,使⽤⽅便
缺点:
- 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)
- 不能注⼊⼀个Final修饰的属性
构造函数注入(Spring 4.X推荐)
优点:
- 可以注⼊final修饰的属性
- 注⼊的对象不会被修改
- 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法是在类加载阶段就会执⾏的⽅法.
- 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
缺点:
- 注⼊多个对象时, 代码会⽐较繁琐
Setter注入(Spring 3.X推荐)
优点:
- ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
缺点:
- 不能注⼊⼀个Final修饰的属性
- 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险
3.8 @Autowired存在问题
当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
程序会出现无法正确找到Bean的报错
如何解决上述问题呢?
Spring提供了以下⼏种解决⽅案:
- @Primary
- @Qualifier
- @Resource
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
使⽤@Qualifier注解:指定当前要注⼊的bean对象。
在@Qualifier的value属性中,指定注⼊的bean的名称(@Qualifier注解不能单独使⽤,必须配合@Autowired使⽤)
1 2 3 4 5 6 7 8 9 10 |
|
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。并且由于@Resource是由JDK提供的,因此在其他框架下也有可延展性。
1 2 3 4 5 6 7 8 9 |
|
@Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
@Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊。相⽐于 @Autowired 来说 @Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean
3.9 循环依赖
要弄清楚循环依赖
1、需要知道Bean初始化的两个阶段
① Bean实例化创建实例对象(new Bean())
② Bean实例对象初始化(DI:注解自动注入)
未看源码之前,其实对循环依赖有一个想法:循环依赖可以看做是一个死锁。
预防死锁的方法:打破死锁的四个必要条件(互斥、请求并等待、不可剥夺、循环等待),由于循环依赖的资源是对象自身,所以常用破坏循环等待条件方法:编号顺序执行,不适用
选择破坏请求并等待条件:先创建对象,再赋值,模型
A a = new A();
B b = new B();
A.b = b;
B.a = a;
研究源码之后发现:想法差不多,但是代码实现非常精彩。模型(打标没想到过)
A a = new A();
B b = new B();
b.a = a;
a.b = b;