主頁 > 軟體設計 > 手寫代碼,簡單實作Spring框架

手寫代碼,簡單實作Spring框架

2020-12-26 13:30:08 軟體設計

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);
	}

	@Override
	public 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 {
	@Autowired
	HelloWord helloWord;
	
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
		System.out.println("Hello Spring!");
	}
	
	@Override
	public 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, 這里引導需要做以下任務:

  1. 創建 Bean 物件(createBeans());
  2. 決議所有 Bean 上的通知(aop());
  3. 給所有的帶有 @Autowire 注解的成員變數賦值(di()) ;
  4. 查找 Bean 里面所有的方法,如果有 @PostConstruct 注解,則執行該方法,相當于是給系統初始化的時候默認執行一些方法(post());
  5. 如果重寫了 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();
        }
    }
}

3.1 創建Bean

根據控制反轉的原理,Bean 的整個生命周期由框架管理,在創建 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", "");
                    if (!clsName.contains("cn.edu.zhku.java.core.advanced.mooc14.annotations")) {
                        Class cls = main.getClassLoader().loadClass(clsName);
                        log("load class: " + cls.getName());
                        classesBuilder.accept(cls);
                    }
                }
            }
        }
    }
    return classesBuilder.build();
}

從剛才的 loadClasses 函式獲取所有非介面的類, 并進行過濾, 只留下帶有 Component 的物件,利用的反射的方法生產該 Bean 物件,這里暫時忽略例外處理,將處理之后獲得的所有物件保存在類的 List 集合變數 beans,

MySpringApplication::createBeans

beans = loadClasses(main).filter(cls ->
            Arrays.stream(cls.getAnnotations()).anyMatch(a -> a instanceof Component)
    ).map(cls -> {
        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 {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("第一個服務啟動,開始執行加載資料等操作,");
    }
}

@Component
@Order(value = 2)
public class StartupRunnerTwo implements CommandLineRunner {

    @Override
    public 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 {
            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;
	}
}

織入由 JDK 動態代理技術來實作,

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 織入切面,

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);
                }
            }
        }
    }
}

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 {
	@Autowired
	HelloWorld helloWorld;
	
	@PostConstruct
	public void post() {
		System.out.println("Greeting Service Impl is ready: " + helloWorld.hello());
	}

	@Override
	public 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 方法來類中使用依賴注入的物件,

MySpring.java

@Component
public class MySpring implements CommandLineRunner {
    @Autowired
    private GreetingService greetingService;

    public static void main(String[] args) {
        MySpringApplication.ENABLE_LOG = false;
        MySpringApplication.run(MySpring.class);
    }
    
    @Override
    public void run() {
        System.out.println("Now the application is running");
        System.out.println("This is my spring");
        greetingService.greet();
    }
}

編譯并運行,輸出資訊

Greeting Service Impl is ready: Hello world!
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()

程式在創建完Bean之后進行了AOP處理并進行依賴注入,投入使用后,從列印資訊中可以看出執行任務的順序與與預期的相符,

小結

這個 MySpring 案例使用了反射、泛型、代理、注解、類加載器等技術,實作了一個高仿Spring的專案,其中最重要的就是注解,有了這些注解,就可以在程式啟動之前利用反射加載這些注解,對程式做很多定制化,反射和注解是框架軟體里面實作的一個重要基礎,MySpring是使用標準的Java代理來做的AOP,那么這些 AOP 的類就要求都需要實作一個介面,對于一些非介面的類,就需要做位元組碼的絞入,這個在上面的代碼并沒有體現,而在標準 Spring 框架中是使用 CGLIB 代理的,

源代碼

地址: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)著.

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/240603.html

標籤:其他

上一篇:【ArcGIS風暴】ArcGIS tif轉jpg:JPEG壓縮僅支持8位或16位無符號資料(具有一個或三個波段,且沒有色彩映射表)解決方案!

下一篇:ABP后臺作業詳細設計簡單分析

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more