001-Readme

源码仓库地址:yelanyanyu/winter-framework (github.com)

基本内容

这个项目预计完成一个小型的类 spring 的项目。要实现的功能是:

开发以及测试环境

Java 17 + tomcat 10.1.17 + servlet-api 6.0 + maven 3.9.4
IDE:IntelliJ IDEA 2023.2 + postman

未来开发规划

目录结构

├─.github
│  └─ISSUE_TEMPLATE
├─.idea
│  ├─artifacts
│  └─inspectionProfiles
├─framework-test # 测试程序
│  └─src
│      └─main
│          ├─java
│          │  └─com
│          │      └─yelanyanyu
│          │          ├─bean
│          │          ├─config
│          │          └─controller
│          ├─resources
│          └─webapp
│              └─WEB-INF # 资源目录
│                  └─views
├─mvc-context # 容器实现
│  ├─logs
│  └─src
│      ├─main
│      │  ├─java
│      │  │  └─com
│      │  │      └─yelanyanyu
│      │  │          ├─annotation # 注解
│      │  │          ├─context # 容器实现
│      │  │          ├─exception # 自定义异常
│      │  │          ├─io # 读取 bean 以及 配置文件相关
│      │  │          └─util # 工具类
│      │  └─resources
│      └─test
│          ├─java
│          │  └─com
│          │      ├─bean
│          │      └─yelanyanyu
│          └─resources
└─mvc-webmvc # web mvc实现
    ├─logs
    └─src
        ├─main
        │  ├─java
        │  │  └─com
        │  │      └─yelanyanyu
        │  │          └─webmvc
        │  │              ├─annotation # 注解
        │  │              ├─bean 
        │  │              ├─exception # 自定义注解
        │  │              ├─util # 工具类
        │  │              └─view # 视图与视图解析器
        │  └─resources
        └─test
            ├─java
            │  └─com
            │      └─yelanyanyu
            │          └─bean
            └─resources

快速开始

本章旨在快速上手该框架。

JSP 开发

项目配置

假设我们要开发一个 jsp 的前后端半分离项目。

  1. 在源目录下新建配置 .../config/WebMvcConfiguration.java,该类用注解 @Configuration 修饰,表示该类是一个配置类;
@Configuration
public class WebConfiguration {
}
  1. 定义视图解析器 InternalResourceViewResolver:
@Configuration
public class WebConfiguration {
    @Bean
    public InternalResourceViewResolver internalResourceViewResolver() {
        InternalResourceViewResolver irv = new InternalResourceViewResolver();
        irv.setPrefix("/WEB-INF/views/");
        irv.setSuffix(".jsp");
        return irv;
    }
}

解读:

  1. 在顶层目录新建一个根配置类 WebConfig,要求其他类都是该配置类的同级,或者子级,比如说:
    image.png
@ComponentScan
@Configuration
@Import(WebMvcConfiguration.class)
@Order(1)
public class WebConfig {
}

解读:


注意

快速编写 Controller

  1. 通过在类上使用注解 @Controller 或者 @RestController 使得该类成为 controller。
    例如:
@Controller
public class TestController {
}
  1. 通过在该类声明一个方法,并且加上注解 @GetMapping 使得声明一个接受以 get 为请求类型的 Handler 方法,通过 @PostMapping 接收 Post 型的请求。例如:
@Controller
public class TestController {
    @GetMapping("/t1")
    public ModelAndView t1() {
        return null;
    }

    @PostMapping("/test/t2")
    public ModelAndView t2() {
        return null;
    }
}

解读:

  1. 我们可以创建 ModelAndView 对象来向 Request 域中添加变量,并且将 request 请求转发到固定资源,或者重定向:
@Controller
public class TestController {
    @GetMapping("/t1")
    public ModelAndView t1() {
	    ModelAndView mv = new ModelAndView("forward:success");
        return mv;
    }

    @PostMapping("/test/t2")
    public ModelAndView t2() {
        ModelAndView mv = new ModelAndView("redirect:success");
        return mv;
    }
}

解读:

基于 JSON 的前后端分离

该项目最主要支持的是 json 数据的返回,从而实现前后端的完全分离。还可以返回二进制流以用于文件的上传和下载。

返回 json 数据的核心是以下几种注解:

RestController

序列化对象

如果一个类被 @RestController 注释,那么该类中的任何方法的返回值都会被序列化为 JSON 格式的数据,返回。举个例子,假如我们有一个对象 Member,如果我们在方法中返回 Member 对象,那么底层就会将其序列化成为 json 数据:

@RestController
public class DemoController {
    @GetMapping("/t3")
    public Member t3() {
        return new Member("asdfasf", 100);
    }
}

返回的数据:

{"name":"asdfasf","id":100}

序列化字符串

如果方法返回的是 String 类型的字符串,那么也会 json 的形式进行解析返回:

@RestController
public class DemoController {
    @GetMapping("/t4")
    public String t4() {
        return "{\"name\":\"asdfasf\",\"id\":100}";
    }
}

返回的数据:

{"name":"asdfasf","id":100}

返回二进制流

由于有文件上传和下载的需求,所以返回非 json 格式的二进制流也是必须的。我们可以通过返回一个 byte 数组也返回二进制流。

@RestController
public class DemoController {
    @GetMapping("/t5")
    public byte[] t5() {
	    // read from something
        return new byte[100];
    }
}

ResponseBody

假如一个类并没有被 @RestController 注释,那么我们仍然可以通过注释 @ResponseBody 来返回 json 格式的数据或者二进制流。

@Controller
public class DemoController {
    @GetMapping("/t3")
    @ResponseBody
    public Member t3() {
        return new Member("asdfasf", 100);
    }

    @GetMapping("/t4")
    @ResponseBody
    public String t4() {
        return "{\"name\":\"asdfasf\",\"id\":100}";
    }

    @GetMapping("/t5")
    @ResponseBody
    public byte[] t5() {
        return new byte[100];
    }
}

解读:

RequestBody

如果有一个方法中的某个参数被 @RequestBody 注释,那么该参数就会封装 json 形式的 request 参数。

方法:

@RestController
@Slf4j
public class DemoController {
    @GetMapping("/t6")
    public Member t6(@RequestBody Member member, @RequestParam("id") Integer id) {
        log.info("id: {}", id);
        return member;
    }
}

请求参数:

body: {"name":"wdnmd","id":100}
url: http://localhost:8080/mvc/t6?id=200

结果返回:

{
    "name": "wdnmd",
    "id": 100
}
console: id: 200

解读:

其他功能

PathVariable

该框架还支持路径变量解析,直接上使用案例:

@RestController
public class DemoController {
    @GetMapping("/t7/{name}/{id}")
    public Member t7(@PathVariable("name") String name, @PathVariable("id") Integer id) {
        return new Member(name, id);
    }
}

我们测试的 url 为:http://localhost:8080/mvc/t7/zhangsan/999
返回的结果为:

{
    "name": "zhangsan",
    "id": 999
}

mvc-context

003-ioc容器
实现该项目的重要组件就是 ioc 容器,ioc 容器保存所有类的映像——bean 实例,并且在配对的请求到来时,调用相应的 controller,还实现了用户可以高度自定义的配置选项。保证了开闭原则。

基本架构

AnnotationConfigApplicationContext.png

解读:

加载 bean 的基本流程

创建 BeanDefinition

一个 bean 通常比较复杂,如果我们只保留 bean 实例 Object 对象,那么信息就太少,会对后面的反射解析,注解解析造成极大的麻烦。所以,我们首先需要抽象出一个 Bean 的定义。

@Data
public class BeanDefinition implements Comparable<BeanDefinition> {
    /**
     * bean实例的名称
     */
    private final String name;
    /**
     * bean 的class对象
     */
    private final Class<?> beanClass;
    /**
     * bean 对象实例
     */
    private Object instance = null;
    /**
     * bean 构造器
     */
    private final Constructor<?> constructor;
    /**
     * 工厂方法名称
     */
    private final String factoryName;
    /**
     * 工厂方法对象
     */
    private final Method factoryMethod;
    /**
     * bean 的顺序
     */
    private final int order;
    /**
     * 是否被 @Primary 注释
     */
    private final boolean primary;
    /**
     * init and destroy
     */
    private boolean init = false;
    private String initMethodName;
    private String destroyMethodName;
    private Method initMethod;
    private Method destoyMethod;
}

为什么要设置工厂方法?因为,Bean 的注入行为有两种,一是直接注释了 @Component 注解的 bean;另一类是配置类(被 @Configuration 注释的)中,被 @Bean 注释的方法,该方法也应该返回一个 bean,被 @Bean 注释的方法就是工厂方法,该工厂方法用于返回 bean 实例,而不是由容器内部创建。

故而,扫描的 bean 就可以大致分为三种情况:

如何扫描 bean 呢?需要提供如下几个信息:

扫描什么资源?以 .class 为结尾的资源。至于为什么,这里涉及到 jvm 的知识就不再过多赘述了。如何扫描所有资源呢?传统的方法是写一个 dfs 深度优先搜索,但是 java 8 的 stream 操作可以极大的简化这一过程。

当所有的资源都扫描完毕后,会以全类名保存,以便之后反射生成实例使用。

初始化 Bean

当所有的 BeanDefinition 都已经创建完毕,那么就需要尝试创建 Bean 实例了。在这个阶段要解决的问题主要是循环依赖的问题。

依赖问题,涉及到的注解有: @Value@Autowired。什么是依赖,这里就不再介绍了。

对于以工厂方法注入的 Bean 和以构造器方法注入的 bean ,需要考虑强依赖循环的问题。对于其他方法注入的 Bean,需要解决弱依赖注入的问题。

要解决依赖问题,最好的办法就是两步走:一,创建实例;二,注入。而强依赖是无法分成两步的,所以,在 bean 初始化的时候,就要去判断是否存在强依赖注入,对于这种情况,就应该直接报错。而,弱循环依赖,就可以使用这种方法解决。

这就是为什么不一步到位的说是“创建 bean”,而一定要说初始化 bean 了。

总而言之,初始化 Bean 的工作就是:

创建 Bean 实例

还有弱依赖的问题需要解决,这一步要完成的工作是创建所有的 Bean。

故而,这阶段的任务是:

BeanPostProcessor 的实现

该类的作用其实就是类的替换,实际环境中,我们会将原来的类替换成一个代理类。

该类型的类的创建时机很重要,通常应该放在 @Configuration 类的后面被注册到 ioc 容器中。因为在后面的 bean 的依赖注入中,需要使用 BeanPostProcessor 来进行类运行类型的替换。

故而,在创建 Bean 实例后,紧接着,就应该调用 BeanPostProcessor 的方法,来进行类的替换。

还有一个重要的问题是,什么时候要拿到原始的对象,什么时候应该拿到代理对象?这里直接说结论:

需要明确的两个需求:一是我们必须想办法要代理对象的方法得到调用;二是,我们需要当代理对象运行到原始对象的方法时,需要依赖注入的对象起到效果。例如,A 对象有代理对象 AP,其中 A 有属性 B。那么当 AP 调用特定方法 m1() 的使用,必定有一步是会执行到 A 定义的方法 m1() 的,假如 A 的方法 m1() 需要使用到属性 B,而属性 B 又没有注入就会出现空指针异常。

基本使用

大体上,spring 框架中的常用功能,这里都有。

但是,有几个功能的应用以及原理是需要特别说明的。

Value

通过该注解,可以读取自定义的配置。你可以通过新建 application.yml 或者 application.properties 来自定义配置。同时,你也可以获取一些系统的默认配置。

注意:

Autowired

由于 spring 框架中注解 @Resource@Autowired 的使用场景和原理都极其类似,所以,我们省略 @Resource 的实现。

注意,@Autowired 不仅仅支持以成员的方法注入,还可以支持在构造器,在工厂方法参数中注入。

PostConstruct

该注解可以用来绑定 init 方法,当 Bean 创建完毕后(包括实例创建,依赖注入后),就会调用这个方法。

Class01 为例:

@Configuration
public class Class01 {
    @PostConstruct
    public void init() {
        System.out.println("++++++++++++++++++");
    }
}

解读:

PreDestroy

该注解用来指示类被销毁(容器被关闭时,由 JVM 自动进行垃圾回收销毁)的时候,调用的方法。

还是以 Class01 为例:

@Configuration
public class Class01 {
    @PreDestroy
    public void destroy() {
        System.out.println("-------------------");
    }
}

当 spring 容器执行 close() 方法的时候,就会自动调用每个组件的该方法,所以对于 Class01,会在控制台输出 -------------------

Bean

使用

Bean 注解只在工厂方法(被 @Configuration 注释的组件)中出现,Bean 注解只能用来修饰方法,被其注释的方法返回的对象会自动注册到 ioc 中管理。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
    String value() default "";

    String initMethod() default "";

    String destroyMethod() default "";
}

解读


实现原理

我们只对如何实现 initMethod 和 destroyMethod 做说明。主要说明,当 @Bean(initMethod = "init", destroyMethod = "destroy") 这个功能是如何实现的。

  1. 当我们创建 BeanDefinition 的时候,我们会找到所有被 @Bean 注释的方法,然后对于扫描的所有方法,我们都会获取 Bean 注解的值,也就是 initMethod 和 destroyMethod 的值,将其以字符串的形式保存到该 Bean 的 BeanDefinition 中,等待到之后调用;
  2. 关键在于是如何通过 name 就调用该方法的;
  3. 调用方法的时机就不过多介绍了,我们重点关注源码中的 callMethod(...) 方法的实现:
/**
     * invoke the init and destroy methods
     *
     * @param instance
     * @param method
     * @param namedMethod
     */
    private void callMethod(Object instance, Method method, String namedMethod) throws InvocationTargetException, IllegalAccessException {
        if (method != null) {
            method.invoke(instance);
        } else if (namedMethod != null) {
            Method mn = ClassPathUtils.findMethodByName(instance.getClass(), namedMethod);
            mn.setAccessible(true);
            mn.invoke(instance);
        }
    }

解读:

mvc-aop

005-aop
使用该类可以实现 spring 中的面向切面编程,我们提供了三个注解 @Around@Before@After,三个注解仅仅只对类生效。即,被任一注解修饰的类内部的所有方法都会被代理。

注解在使用的时候,需要指定相应的 Handler bean name。用户需要自行实现 Handler,即实现 InvocationHandler 接口实现 around 逻辑或者继承我们提供的 AfterInvocationHandlerAdapter 实现 after 的逻辑,继承 BeforeInvocationHandlerAdapter 实现 before 的逻辑。

最后需要将这些 handler 都用注解 @Component 注释,将其加入 ioc 中。

mvc-jdbc

006-JDBC与事务
提供了 JdbcTemplate 来简化数据库操作。

提供了注解 @Transactional 注解来实现声明式事务,仅仅只在类上使用。使用方法与 spring 完全一样。

默认的传播模式为 REQUIRED。出错就全局回滚。这是默认的模式。

mvc-boot

使用内嵌的 tomcat 完全不同通过 web.xml 就可以启动一个 web 项目。支持,直接使用 java -jar xxx.war 的方式启动 web 项目。

具体的使用细节,可以看 007-boot