手写代码,简单实现Spring框架
Java核心编程高阶实战案例:MySpring
本博文通过学习 中国大学MOOC 平台上陈良育老师讲的 Java核心技术(高阶) 课程,因为老师视频中的讲解有些较为简略,于是我自己另外搜集资料,从老师提供的代码中剖析,再深入研究与应用,并记录于该博文中。
本文涉及知识点:
代理,反射,泛型,注解,类加载器,IoC/DI,AOP,lamdba表达式,stream流等
一、了解Spring
Spring 框架是 Java EE 中最优秀的框架。Spring 框架的核心是基于 控制反转(Inversion of Control, IoC) 的原理。在 Spring 中,控制反转是 Spring 的重要组成部分,Spring 实现的核心是基于 依赖注入(dependency injection, DI) 。Spring 框架的两个核心功能是 依赖注入 和 面向切面编程(Aspect Oriented Programing, AOP) 。
Spring Boot 项目旨在简化使用Spring 构建应用程序的入门体验。SpringBoot 框架中有两个非常重要的策略:开箱即用 和 约定优于配置。从最根本上来讲,Spring Boot 就是一些库的集合,它能够被任意项目的构建系统所使用。它提供了配置好的 starter
依赖来简化构建配置。简便起见,该框架也提供了命令行界面,它可以用来运行和测试Boot应用。
1.1 控制反转和依赖注入
控制反转的核心是依赖注入,旨在提供一种更简单的机制来设置组件依赖项(通常称为对象的协作者),并在整个生命周期中管理这些依赖项。IoC可以分解为两种子类型:依赖注入和依赖查找。因此,依赖注入是IoC的一种特殊形式,一般情况下,这两个个术语可以互换使用。需要依赖项的组件通常被称为依赖对象,或者在IoC的情况下被称为目标对象。
1.2 AOP相关概念
与大多数技术一样,AOP 带有自己特定的一组概念术语,了解它们的含义非常重要。
- 连接点(JoinPoint):连接点是应用程序执行期间明确定义的一个点。连接点是AOP的核心概念,并且定义了在应用程序中可以使用 AOP 插入其他逻辑的点。连接点可以使用在方法调用、方法调用本身、类初始化和对象实例化等。
- 通知(Advice):在特定连接点执行的代码就是通知,它是由类中的方法定义的。有许多类型的通知,比如前置通知(在连接点之前执行)和后置通知(在连接点之后执行)。
- 切入点(Pointcut):切入点是用于定义何时执行通知的连接点集合。通过创建切入点,可以更细致地控制如何将通知应用于应用程序中的组件。
- 切面(Aspect):切面是封装在类中的的通知和切入点的组合。这种组合定义了应该包括在应用程序中的逻辑以及应该执行的位置。
- 织入(Weaving):织入是在适当的位置将切面插入到应用程序代码中的过程。对于编译时 AOP 解决方案,织入过程通常在生成过程时完成。同样,对于运行时AOP解决方案,织入过程在运行时动态执行。此外,AspectJ 还支持另外一种称为加载时织入(LTW)的织入机制,在该机制中,拦截底层的 JVM 类加载器,并在类加载器加载字节码时向其提供织入功能。
- 目标对象(Target):执行流由 AOP 进程修改的对象被称为目标对象。
- 引入(Introduction):这是通过引入其他方法或字段来修改对象结果的过程。可以通过引入AOP来使任何对象来实现特定的接口,而无需对象类显式地实现该接口。
Spring AOP 中最明显的简化是只支持一种连接点类型:方法调用。方法调用连接点是迄今为止最有用的连接点,使用它可以实现AOP在日常编程中许多有用的任务。如果需要使用除方法调用之外的连接点通知一些代码,那么可以一起使用Spring和AspectJ。
1.3 代理
Spring AOP 的核心架构基于代理。Spring有两个代理实现:JDK 动态代理和 CGLIB 代理。默认情况下,当被通知的目标对象实现一个接口时,Spring 将使用 JDK 动态代理来创建目标的代理实例。但是,当被通知目标对象没有实现接口(例如,它是一个具体的类)时,将使用 CGLIB 来创建代理实例。一个主要的原因是 JDK 动态代理仅支持接口代理。
Java 中的代理包括静态代理和动态代理。静态代理的特点是代理者和被代理者的关系在编译期间就已经确认了,即代理类在程序运行的时候就存在;而动态代理的特点是代理者和被代理者的关系要在程序运行期间才能确认,即代理类在程序运行前是不存在的,代理类是在程序运行时根据代码的“指示”动态生成的。动态代理比静态代理的优势在于,动态代理可以很方便地对代理类的函数进行统一的处理(invoke),而不是修改每个代理类的函数,更灵活,易于扩展。
JDK 的动态代理以实现接口的方式,利用 java 的反射机制来实现的。JAVA反射机制是动态获取信息以及动态调用对象方法的功能,在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。在Java的动态代理中,有两个重要的类或接口,一个是InvocationHandler 接口,另一个是Proxy类。InvocationHandler 接口是给动态代理类实现的,负责处理被代理对象的操作。Proxy 类是用来创建动态代理类实例对象的,只有得到这个对象,才能调用需要代理的方法。动态代理的代理类是在静态代理类上进行修改,将动态代理类实现 InvocationHandler 接口,重写 Invoke 方法,Invoke 方法通过传入的被代理类方法和参数来执行。
CGLIB 代理以继承类的方式,利用 FastClass 机制来实现的。在 CGLIB 代理机制中,CGLIB 会为每个代理动态生成新类的字节码,并尽可能重用已经生成的类。因为 CGLIB 是继承机制,所以CGLIB无法代理被final修饰的方法。
二、构建 Spring 的示例应用程序
从官方文档中学习SpringBoot:https://spring.io/projects/spring-boot#learn
首先引入 Spring 的依赖。如果没有依赖项管理工具,那么应用程序中选择所需使用的模块就会比较麻烦,这里使用 Maven 来管理 Java 应用程序依赖项。 这里引用的依赖十分简单,就只有一个 Spring Boot 的 starter。为了更容易地管理依赖版本和使用默认配置,框架提供了一个parent,工程可以继承它。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.1</version><relativePath/> <-!-- lookup parent from repository -></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency></dependencies></project>
前面引入的依赖为程序提供了基础,接下来为项目增加可执行代码。编写一个引导类源文件,编译之后就可以运行项目了,这里写个最简单的应用程序。
DemoApplication.java
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);System.out.println("Hello Spring!");}
}
@SpringBootApplication 被用于激活 @EnableAutoConfiguration、@ComponentScan 和 @Configuration 三个注解的特性。其中@EnableAutoConfiguration 负责激活 SpringBoot 自动装配机制, @ComponentScan 激活 @Component 的扫描 ,@Configuration 声明被标注为配置类。@SpringBootApplication 注解等同于这三个注解。
启动运行后显示 Spring 的相关信息,并且打印出Hello Spring!
。
. ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v2.4.1)2020-12-23 15:35:15.786 INFO 6600 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 1.8.0_201 on LAPTOP-Ting with PID 6600 (D:\IdeaProjects\demo\target\classes started by Oseting in D:\IdeaProjects\demo)
2020-12-23 15:35:15.789 INFO 6600 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2020-12-23 15:35:16.518 INFO 6600 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.339 seconds (JVM running for 2.003)
Hello Spring!
如果是使用控制台命令的话,那么实现 CommandLineRunner 这个类,并重写run方法即可。运行结果会跟上面输出的信息一样。
DemoApplication.java
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@Overridepublic void run(String... args) throws Exception {System.out.println("Hello Spring!");}
}
注意,在启动类中编写一些代码可以实现一些任务,但是如果涉及到依赖一些 Spring 管理的目标对象,那么在 main 函数里面就不合适了,因为 main 函数是静态方法,只能调用静态成员变量,而通过依赖注入创建的对象显然不是静态的,如果在 main 函数里进行依赖注入,那么将会出现编译错误。当我们需要在启动类中依赖注入怎么办?有个解决办法是继承 CommandLineRunner 类,重写其中的run方法,引导类启动时调用run方法执行。程序中需要依赖注入的目标对象就可以放在这个地方执行任务。
这里写个Hello World 应用程序。
HelloWord.java
@Component
public class HelloWord {public void sayHi() {System.out.println("Hello World!");}
}
DemoApplication.java
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {@AutowiredHelloWord helloWord;public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);System.out.println("Hello Spring!");}@Overridepublic void run(String... args) throws Exception {helloWord.sayHi();}
}
启动后输出
/* 省略 Spring 启动信息 */
Hello World!
Hello Spring!
这里要注意run方法是在启动时执行的,所以会先打印出Hello World!
,启动完成之后才继续执行打印Hello Spring!
。
三、编写代码实现MySpring
这节来演示自己动手写代码实现简单的 Spring 框架,实现过程主要模仿 Spring Boot 的结构,暂且将这个自定义框架命名为 MySpring 吧!
从整体上看,MySpring 框架要做到以下几点:
- 使用注解标注需要创建的Bean
- 构建Bean容器
- 对成员变量初始化,实现IoC/DI
- 使用代理,解决方法的Aspect注解调用
为了让框架支持命令行执行,并且让启动类支持使用依赖注入的对象,这里需要创建一个 CommandLineRunner
接口,定义一个 run
函数,引导类将在启动的时候执行 run 函数。引导类实现该接口,重写 run 函数,就可以自定义所需执行的任务。
CommandLineRunner.java
public interface CommandLineRunner {void run();
}
然后创建 MySpring 的引导类。引导类将会做引导一系列工作,其核心就是实现IoC和AOP,包括创建并管理Bean对象、通过代理实现AOP。 这里引导需要做以下任务:
- 创建 Bean 对象(createBeans());
- 解析所有 Bean 上的通知(aop());
- 给所有的带有
@Autowire
注解的成员变量赋值(di()) ; - 查找 Bean 里面所有的方法,如果有
@PostConstruct
注解,则执行该方法,相当于是给系统初始化的时候默认执行一些方法(post()); - 如果重写了 CommandLineRunner 类里面的 run 函数,那么就执行 run 函数。
MySpringApplication.java
public class MySpringApplication {private CommandLineRunner runner;public static void run(Class main) {MySpringApplication app = new MySpringApplication();app.createBeans(main);app.aop();app.di();app.post();System.out.println("My Spring init successfully!");if (app.runner != null) {app.runner.run();}}
}
为方便之后代码的调试工作,再写一个打印日志的函数,通过一个变量作为调试日志输出开关。
MySpringApplication.java
public static boolean ENABLE_LOG = true;
private static void log (Object msg) {if (ENABLE_LOG) {System.out.println(msg);}
}
3.1 创建Bean
在标准 Spring中 初始化 Bean的流程是:
资源定位
—> Bean定义
—> 发布Bean定义
—> 实例化
—> 依赖注入
—> ……
根据控制反转的原理,Bean 的整个生命周期由框架管理,依赖注入会在后序3.3节完成。在创建 Bean 对象之前,需要知道哪些 Bean 将会被框架管理,利用注解标识哪些类需要产出对象。这里先写好注解,标注哪些类要注入到容器中进行管理的,模仿 Spring 框架,这里同样创建一个 @Component
的注解。注解的内容为空,因为目前该注解仅起到标识作用。
Component.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Component { }
引导类中的 main 是整个程序的入口,框架根据 main 类,去加载相应的 Bean。从引导类中获取存放加载的目录队列,遍历所有目录下所有子目录里面所有字节码文件(*.class),忽略所有的接口,因为接口不能实例化,然后通过Stream返回这些类文件。
MySpringApplication.java
private Stream<Class> loadClasses(Class main) throws MalformedURLException, ClassNotFoundException {URL resource = main.getResource("");File baseDir = new File(resource.getFile());Queue<File> dirs = new LinkedList<>();dirs.add(baseDir);int offset = main.getResource("/").getPath().length();Stream.Builder<Class> classesBuilder = Stream.builder();while (!dirs.isEmpty()) {File tmp = dirs.poll();for (File f : tmp.listFiles()) {if (f.isDirectory()) {dirs.add(f);} else {if (f.getName().endsWith(".class")) {String clsName = f.toURI().toURL().getPath().substring(offset).replaceAll("/", ".").replace(".class", "");Class cls = main.getClassLoader().loadClass(clsName);if (!cls.isInterface()) {log("load class: " + cls.getName());classesBuilder.accept(cls);}}}}}return classesBuilder.build();
}
从刚才的 loadClasses 函数获取所有非接口的类, 并进行过滤, 只留下带有 Component 的对象,利用的反射的方法生产该 Bean 对象。这里暂时忽略异常处理。将处理之后获得的所有对象保存在类的 List 集合变量 beans。
MySpringApplication.java
private List<Object> beans;
MySpringApplication::createBeans
beans = loadClasses(main).filter(cls ->Arrays.stream(cls.getAnnotations()).anyMatch(a -> a instanceof Component)).map(cls -> {log("create class: " + cls.getName());return cls.getConstructor().newInstance();}).collect(Collectors.toList());
留下第一个既有 @Component
注解的, 又有 @CommandLineRunner
接口的对象, 作为 runner(也是类里面的 CommandLineRunner 对象)。这里的处理是为了之后能在引导类执行的任务流中自定义任务用的。
MySpringApplication::createBeans
runner = (CommandLineRunner) beans.stream().filter(bean ->Arrays.stream(bean.getClass().getInterfaces()).anyMatch(i -> i.equals(CommandLineRunner.class))).findFirst().orElse(null);
为了简单起见,MySpring仅获取第一个第一个既有
@Component
注解的, 又有@CommandLineRunner
接口的对象。在Spring中,当服务中有多个CommandLineRunner对象时,默认情况下是按照自然顺序执行的。可以通过@Order指定执行顺序。@Component @Order(value = 1) public class StartRunnerOne implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("第一个服务启动,开始执行加载数据等操作。");} }@Component @Order(value = 2) public class StartupRunnerTwo implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("第二个服务启动,开始执行加载数据等操作。");} }
整个 createBeans 函数
MySpringApplication.java
private List<Object> beans;
private CommandLineRunner runner;
private void createBeans(Class main) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, MalformedURLException, ClassNotFoundException {beans = loadClasses(main).filter(cls ->Arrays.stream(cls.getAnnotations()).anyMatch(a -> a instanceof Component)).map(cls -> {try {log("create class: " + cls.getName());return cls.getConstructor().newInstance();} catch (InstantiationException | IllegalAccessException | IllegalArgumentException| InvocationTargetException | NoSuchMethodException | SecurityException e) {throw new IllegalStateException(e);}}).collect(Collectors.toList());runner = (CommandLineRunner) beans.stream().filter(bean ->Arrays.stream(bean.getClass().getInterfaces()).anyMatch(i -> i.equals(CommandLineRunner.class))).findFirst().orElse(null);
}
3.2 实现AOP
根据 AOP 的概念,创建一些 AOP 注解来对 MySpring 管理的 Bean 进行标识。简单起见,这里需要创建的注解仅有通知(前置通知Before和后置通知After)和切面(Aspect)。同样的,MySpring 的注解名字尽量与 Spring 的一致。
创建通知注解Before 和 After,这两个注解可以分别在创建 Bean 的前后执行任务。
Before.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Before {String value();
}
After.java
@Retention(RetentionPolicy.RUNTIME)
public @interface After {String value();
}
创建切面 Aspect 注解,用来表示哪些类属于切面类,切面类在类中封装了一些通知和切入点的组合。
Aspect.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect { }
创建一个 AdviceEnum 枚举类,表示 AOP 中通知所包含的类型。目前 MySpring 只有两种通知:前置通知(before)和后置通知(after)。
AdviceEnum.java
public enum AdviceEnum {BEFORE, AFTER
}
定义一个 AOP 的实体类,用来描述切面的信息,包括连接点(目标对象类与类名,目标对象方法与方法名)和通知(前置通知和后置通知)。
MyAop.java
public class MyAop {private final String target;private final String targetMethod;private final Object aspect;private final AdviceEnum advice;private final Method method;public MyAop(String target, String targetMethod, Object aspect, AdviceEnum advice, Method method) {super();this.target = target;this.targetMethod = targetMethod;this.aspect = aspect;this.advice = advice;this.method = method;}public String getTarget() { return target; }public String getTargetMethod() { return targetMethod; }public Object getAspect() { return aspect; }public AdviceEnum getAdvice() { return advice; }public Method getMethod() { return method; }
}
实现AOP织入。关键是利用反射修改目标对象的字节码文件,核心代码是aop.getMethod().invoke(aop.getAspect(), m, args)
,表示指定参数在在指定的对象上调用指定的方法,也就是说在目标对象上执行自定义的任务。AOP的通知来决定织入的时间。
MySpringApplication.java
private void runAop(List<MyAop> aops, AdviceEnum advice, Method m, Object... args) {if (aops != null) {aops.stream().filter(a -> a.getAdvice() == advice).forEach(aop -> {try {aop.getMethod().invoke(aop.getAspect(), m, args);} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {throw new IllegalStateException(e);}});}
}
遍历所有 MySpring 管理的 Bean,检查是否有 @Aspect
注解,如果有,则该 Bean 是个切面的类,那么遍历该 Bean 的所有方法,解析 Bean 上的通知,将每个通知用 MyAOP 类封装信息并生成一个对象(将注解中目标对象的类名和方法名分开)。
MySpringApplication::aop
Stream.Builder<MyAop> myAopsBuilder = Stream.builder();
for (Object bean: beans) {boolean isAspect = Arrays.stream(bean.getClass().getAnnotations()).anyMatch(a -> (a instanceof Aspect));if (isAspect) {for (Method m: bean.getClass().getMethods()) {for (Annotation a: m.getAnnotations()) {String pointCut = null;AdviceEnum advice = null;if (a instanceof Before) {pointCut = ((Before)a).value();advice = AdviceEnum.BEFORE;}if (a instanceof After) {pointCut = ((After)a).value();advice = AdviceEnum.AFTER;}if (pointCut != null) {int sep = pointCut.lastIndexOf(".");String targetClass = pointCut.substring(0, sep);String targetMethod = pointCut.substring(sep + 1);myAopsBuilder.accept(new MyAop(targetClass, targetMethod, bean, advice, m));}}}}
}
通过 JDK 的动态代理(依赖于接口)来把 MySpring 管理的 Bean 织入切面。如果类实现了接口,则JDK对该类进行代理,使用JDK的 Proxy 类 newProxyInstance
函数创建代理对象。
MySpringApplication::aop
private Object[] proxyBeans;
Map<String, Map<String, List<MyAop>>> clsMethodAopMapping = myAopsBuilder.build().collect(Collectors.groupingBy(MyAop::getTarget,Collectors.groupingBy(MyAop::getTargetMethod)));proxyBeans = new Object[beans.size()];
for (int i = 0; i < beans.size(); i++) {Object bean = beans.get(i);String clsName = bean.getClass().getName();if (clsMethodAopMapping.containsKey(clsName)) {Class[] interfaces = bean.getClass().getInterfaces();if (interfaces.length > 0) {Object proxyInstance = Proxy.newProxyInstance(bean.getClass().getClassLoader(),interfaces, (proxy, method, args) -> {String methodName = method.getName();List<MyAop> aops = clsMethodAopMapping.get(clsName).get(methodName);runAop(aops, AdviceEnum.BEFORE, method, args);Object res = method.invoke(bean, args);runAop(aops, AdviceEnum.AFTER, method, args);return res;});proxyBeans[i] = proxyInstance;}}
}
3.3 依赖注入
完成 Bean 的创建和切面的处理之后,需要将代理的 Bean 对象注入到另外需要依赖的 Bean 中。
先创建依赖注入的注解,用于标识哪些类需要注入对象。同样的,这里只有标识作用,所以这里也是空内容。
Autowired.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired { }
对 Bean 赋值,根据不同 Bean 类型做不同的处理。如果是接口,就根据 Bean 的接口判定来赋予代理对象。
MySpringApplication.java
private Object getRequiredInterfaceBean(Class cls) {for (int i = 0; i < beans.size(); i++) {Object bean = beans.get(i);for (Class beanI: bean.getClass().getInterfaces()) {if (beanI.equals(cls)) {return proxyBeans[i] != null ? proxyBeans[i] : bean;}}}return null;
}
如果不是接口,则直接赋予对象。
MySpringApplication.java
private Object getRequiredBean(Class cls) {return beans.stream().filter(bean -> bean.getClass().equals(cls)).findFirst().orElse(null);
}
遍历所有的 Bean,获取每个 Bean 的成员变量,如果成员变量是带@Autowired
注解,则给所有的 @Autowire
成员变量赋值,如果获取对象成功,就给成员变量赋值。
MySpringApplication.java
private void di() throws IllegalArgumentException, IllegalAccessException {for(Object bean: beans) {for (Field f: bean.getClass().getDeclaredFields()) {if (Arrays.stream(f.getAnnotations()).anyMatch(a -> (a instanceof Autowired))) {Class fCls = f.getType();Object requiredBean;if (fCls.isInterface()) {requiredBean = getRequiredInterfaceBean(fCls);} else {requiredBean = getRequiredBean(fCls);}if (requiredBean != null) {f.setAccessible(true);f.set(bean, requiredBean);log(String.format("Field %s has annotation Autowired, execute di, %s", f.toString(), requiredBean));}}}}
}
3.4 执行默认方法
在完成依赖注入之后,代理产生的 Bean 对象就基本可以投入使用了,在使用的时候 Bean 的整个生命周期仍然归属 MySpring 管理,在使用的之前执行一些默认方法,也可以自己再添加一些自定义方法,这里通过 @PostConstruct
注解来标识使用之前执行的方法。
PostConstruct.java
@Retention(RetentionPolicy.RUNTIME)
public @interface PostConstruct { }
MySpringApplication.java
private void post() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {log("Start Post Processes:");for (Object bean: beans) {for (Method m: bean.getClass().getMethods()) {if (Arrays.stream(m.getAnnotations()).anyMatch(a -> (a instanceof PostConstruct))) {m.setAccessible(true);m.invoke(bean);}}}
}
至此,MySpring 框架的核心搭建好了。
3.5 测试MySpring
框架构建完成之后,使用一个简单的例子来测试框架是否能够运行并且与预期相符。
创建一个简单的类,并通过 hello 函数打印出Hello World!
信息。使用 @Component
注解标识当前类对象的生命周期是由 MySpring 容器管理。
HelloWorld.java
@Component
public class HelloWorld {public String hello() {return "Hello world!";}
}
再创建一个接口,里面包含一个简单的问候的方法。
GreetingService.java
public interface GreetingService {void greet();
}
创建一个实现类并添加 @Component
注解,重写 greet 方法。使用依赖注入的注解 @Autowired
,标识当前成员变量由容器自动装配合适对象。并且添加一个使用 @PostConstruct
来标识方法,使对象在生产之后,使用之前执行一些特定的任务。
GreetingServiceImpl.java
@Component
public class GreetingServiceImpl implements GreetingService {@AutowiredHelloWorld helloWorld;@PostConstructpublic void post() {System.out.println("Greeting Service Impl is ready: " + helloWorld.hello());}@Overridepublic void greet() {System.out.println("Hello CSDN!");}
}
创建一个切面,使用 @Aspect
注解标识该类为切面,并且由MySpring管理,将类里面的连接点和通知按需求织入Bean中。
GreetingServiceAspect.java
@Aspect
@Component
public class GreetingServiceAspect {@Before(value = "cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl.greet")public void beforeAdvice(Method method, Object... args) {System.out.println("Before method:" + method);}@After(value = "cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl.greet")public void afterAdvice(Method method, Object... args) {System.out.println("After method:" + method);}
}
创建引导类,引导类也是一个 Bean,同样归属框架管理。这里实现 MySpring 框架的 CommandLineRunner
类并重写 run 方法来类中使用依赖注入的对象。开启日志,设置 ENABLE_LOG = true
。
MySpring.java
@Component
public class MySpring implements CommandLineRunner {@Autowiredprivate GreetingService greetingService;public static void main(String[] args) {MySpringApplication.ENABLE_LOG = true;MySpringApplication.run(MySpring.class);}@Overridepublic void run() {System.out.println("Now the application is running");System.out.println("This is my spring");greetingService.greet();}
}
编译并运行,输出信息
load class: cn.edu.zhku.java.core.advanced.mooc14.AdviceEnum
load class: cn.edu.zhku.java.core.advanced.mooc14.MyAop
load class: cn.edu.zhku.java.core.advanced.mooc14.MySpring
load class: cn.edu.zhku.java.core.advanced.mooc14.MySpringApplication
load class: cn.edu.zhku.java.core.advanced.mooc14.aop.GreetingServiceAspect
load class: cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl
load class: cn.edu.zhku.java.core.advanced.mooc14.di.HelloWorld
create class: cn.edu.zhku.java.core.advanced.mooc14.MySpring
create class: cn.edu.zhku.java.core.advanced.mooc14.aop.GreetingServiceAspect
create class: cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl
create class: cn.edu.zhku.java.core.advanced.mooc14.di.HelloWorld
cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl
Field private cn.edu.zhku.java.core.advanced.mooc14.di.GreetingService cn.edu.zhku.java.core.advanced.mooc14.MySpring.greetingService has annotation Autowired, execute di, cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl@1f17ae12
Field cn.edu.zhku.java.core.advanced.mooc14.di.HelloWorld cn.edu.zhku.java.core.advanced.mooc14.di.GreetingServiceImpl.helloWorld has annotation Autowired, execute di, cn.edu.zhku.java.core.advanced.mooc14.di.HelloWorld@c4437c4
Start Post Processes:
Greeting Service Impl is ready: Hello world!
My Spring init successfully!
Now the application is running
This is my spring
Before method:public abstract void cn.edu.zhku.java.core.advanced.mooc14.di.GreetingService.greet()
Hello CSDN!
After method:public abstract void cn.edu.zhku.java.core.advanced.mooc14.di.GreetingService.greet()
程序在读取类的时候查看是否有@compent
,有则创建对象,所以程序读取了7个类但是只创建了4个对象。创建完之后进行AOP处理并进行依赖注入,然后 Bean 就可以投入使用了。从打印信息中可以看出执行任务的顺序与与预期的相符。
四、小结
这个 MySpring 案例使用了反射、泛型、代理、注解、类加载器等技术,实现了一个高仿Spring的项目。其中最重要的就是注解,有了这些注解,就可以在程序启动之前利用反射加载这些注解,对程序做很多定制化。反射和注解是框架软件里面实现的一个重要基础。MySpring是使用标准的Java代理来做的AOP,那么这些 AOP 的类就要求都需要实现一个接口。对于一些非接口的类,就需要做字节码的绞入,这个在上面的代码并没有体现,而在标准 Spring 框架中是使用 CGLIB 代理的。
在上面的案例中测试MySpring是正面测试 MySpring 的,缺少反面验证的例子,不能说明JDK的动态代理必须都要实现一个接口。
附录
- 源代码:点击下载MySpring案例源代码
参考资料
[1] 中国大学mooc. Java核心技术(高阶). 陈良育
https://www.icourse163.org/learn/ECNU-1206500807#/learn/announce
[2] Spring. https://spring.io
[3] 《Spring 5 高级编程》(第5版). Iuliana Cosmina. Rob Harrop. Chris Schaefer. Clarence Ho. 著;王净 译.
[4] 《Spring Boot 编程思想》(核心篇). 小马哥(mercyblitz)著.
手写代码,简单实现Spring框架相关推荐
- 如何手写一个简单的RPC框架
http://www.czhenblog.cn/article?articleId=29
- jquery手写轮播图_用jQuery如何手写一个简单的轮播图?(附代码)
用jQuery如何手写一个简单的轮播图?下面本篇文章通过代码示例来给大家介绍一下.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 用 jQuery 手写轮播图 先上个效果截图: 主要 ...
- 手写一个简单的IOC容器
手写一个简单的IOC容器 原文 http://localhost:4000/2020/02/25/SSM/spring/%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E5% ...
- 页面如何手写文字html,html手写代码学习笔记
01-01.什么是HTML 整个网页加载时最先执行的代码head,在英文里面是头部的意思. 在英文里面body是身体的意思.整个网页的主体内容. 01-02.什么是属性 举个例子,什么是属性.一辆汽车 ...
- 深入浅出 TCP/IP 协议栈丨手写代码实现网络协议栈
TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输.TCP/IP 协议采用4层结构,分别是应用层.传输层.网络层和链路 ...
- 多元线性回归,岭回归,lasso回归(具体代码(包括调用库代码和手写代码实现)+一点点心得)
最近数据挖掘导论老师布置了一项作业,主要就是线性回归的实现,笔者之前听过吴恩达的线性回归的网课,但一直没有进行代码的实现,这次正好相对系统的整理一下,方便各位同学的学习,也希望能够对其进行优化,优化的 ...
- ClownFish:比手写代码还快的通用数据访问层
最近花了二个月的业余时间重写了我以前的通用数据访问层, 由于是重写,所以我给这个项目取了个新名字:ClownFish 如果需要了解ClownFish的使用方法,请点击ClownFish 使用说明 Cl ...
- 基于bio手写实现简单的rpc
基于bio手写实现简单的rpc 1.bio基础知识 Java BIO:传统的网络通讯模型,就是BIO,同步阻塞IO, 其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket ...
- 揭秘 ClownFish 比手写代码还快的原因
说明:本文的第一版由于反对人数较多(推荐/反对数量是:23 / 17), 我在8月20日删除了博文内容,只留下一段简单的内容. 既然分享技术也引来这么多的反对,那我就不分享了. 如果希望知道我的优化方 ...
最新文章
- linux进程控制程序设计论文,嵌入式,linux进程控制程序设计
- 大脑如何判断该睡觉了?可能是这80种蛋白说了算
- 【福利】赠书:Spring Cloud与Docker微服务架构实战(第2版)
- 2021前端高频面试题整理,附答案
- java 注解应用技巧_改善Java应用程序性能的快速技巧
- LeetCode-18-4Sum
- kotlin中判断字符串_Kotlin程序删除字符串中所有出现的字符
- 软件工程——快速掌握面向对象开发方法
- ubuntu 下groovy 安装配置
- Ubuntu linux 查看串口连接信息
- 直播间越播越没人,大部分刚开始做直播电商的人都会这样
- 广东地下水资源摘录(早期版的)
- 第二节20181110
- Win10镜像安装pytorch-gpu版
- hive plsql使用示例
- 研究生自然辩证法试题题库及答案
- 别说理科男不懂撩妹,这个老司机一生只爱两样:物理和18岁的少女
- java如何导出excel_JAVA如何导出EXCEL表格
- Gradient vanishing and explosion
- 如何获取Intel网卡驱动的源代码
热门文章
- 线性规划中的人工变量与松弛变量
- 奔腾G7400怎么样 相当于什么水平
- 《战锤》武器设计:手绘风格的PBR流程
- TypeError TypeError: can only concatenate str (not “NoneType“) to str
- html文本框怎么写表情,js文本框插入表情支持解析代码
- Microsoft Dynamics CRM 2013 安装过程图解及安装序列号
- 100个常用大数据词汇中英文对照表
- 如何把清晰的CAD图纸转到Word文档里面且背景是白色的
- 链券——区块链世界下的卡券
- python使用pie()函数绘制饼图