手写SSM框架

本文介绍了手写Summer框架,一个模拟Spring、SpringMVC和MyBatis功能的JavaEE开发框架,通过源码实现展示了核心模块如IOC、AOP、MyBatis和Web模块,以及AOP日志记录和整合SSM的方式。
摘要由CSDN通过智能技术生成

Summer框架

通过阅读SSM框架相关书籍,文章,源码从而仿照了一个实现了大部分功能的SSM框架。通过学习这个框架我们可以对SSM框架原理有更深入的理解,并且能够运用到非常多的Java基础知识以及设计模式的运用。
github地址:手写SSM框架

关于

Summer 框架是一个仿真的轻量级 JavaEE 开发框架,旨在模拟实现了 SpringSpringMVCMyBatis 的核心功能,提供了IOCAOP半ORMWeb 开发支持等特性,基本实现了SSM的主要功能

模块介绍

context模块

这是项目的核心模块,在这个模块实现了一个简单的IOC容器。在这里我们没有实现过时的XML配置而仅通过注解配置来实现Bean的注册,除此之外IOC容器还支持YamlXML格式的配置信息读取、BeanPostProcessor、BeanFactoryPostProcessor、Aware等扩展机制。

过程文档

AOP模块

基于JDKbytebuddy动态代理技术和IOC提供的BeanPostProcessorAware机制实现的AOP。使用了责任链模式回溯算法解决了一个PointCut对应多个Advice的调用逻辑。

过程文档

Mybatis模块

基于JDK动态代理机制的半ORM框架,支持XML配置文件和注解开发。并且基于IOC的BeanFactoryPostProcessor等扩展机制实现了与IOC模块的整合。

过程文档

Web模块

基于Servlet实现的Web框架。通过实现Servlet提供的ServletContainerInitializer接口加载载父子容器和DispatcherServlet,并使用FactoryBean机制初始化DispatcherServlet的各种组件如HandlerMapping、HandlerMappingAdapter等。

过程文档:

框架启动过程分析

框架启动的过程总结起来就是:

启动Tomcat时使用ServletContainerInitializer接口加载IOC父子容器和DispatcherServlet。

下面我们来分析一下启动的详细流程:

  1. 启动Tomcat时调用WebApplicationInitializer实现类的onStartup
  2. onStartup方法中向ServletContext容器中注册一个监听器ContextLoaderInitializer持有子容器的DispatcherServlet
  3. ContextLoaderInitializerDispatcherServlet组件的初始化
    • ContextLoaderInitializer:在初始化方法中将根IOC容器保存到ServletContext中。
    • DispatcherServlet:在初始化方法中设置子容器的Parent属性为保存的根容器并刷新(Refresh),然后初始化处理请求的组件。

注意:我们知道ApplicationContext容器在初始化阶段会将需要的Bean全部创建完成,但我们这里需要使用父子结构(父容器感知不到子容器,子容器能够感知父容器),因此我们的子容器会在设置其Parent属性后再进行Refresh加载来感知父容器的Bean

根容器:管理Service和Mapper的Bean容器。

子容器:管理Controller的Bean容器。

两个容器启动扫描的包需要用户继承抽象类AbstractAnnotationConfigDispatcherServletInitializer并重写两个抽象方法来分别指定父子容器的配置入口类。

@Nullable
//指定父容器的配置入口类(Service,Mapper)
protected abstract Class<?>[] getRootConfigClasses();

@Nullable
//指定子容器的配置入口类(Controller)
protected abstract Class<?>[] getServletConfigClasses();

Web请求处理流程

要处理请求,我们主要需要解决下面的问题:

  • 如何保存处理请求的Handler,如何匹配对应的Handler

我们使用一个组件HandlerMapping,解析Controller中的全部Handler,并按照一定规则去匹配Handler。

HandlerController里面的@RequestMapping标志的每一个方法都是一个Handler

  • 如何处理参数和返回值

因为Handler包含的只是Controller方法的一些信息,并不能直接对请求进行处理,因此我们将通过一个Adapter来根据Handler的信息和RequestResponse请求信息进行参数的处理调用以及返回值的处理。

下面是处理的流程,与SpringMVC基本相似:

  1. 前端的所有请求都被一个Servlet也就是DispatcherServlet拦截。
  2. 所有类型的Request都由DispatcherServletdoService方法来进行处理。
  3. 通过handlerMapping组件来根据RequestURL来匹配对应的Handler(包括拦截器)。
  4. 根据获取的HandlerRequestResponse构建handlerAdapter拿到ModelAndView,这里又包含了参数的处理和返回值的处理
    • 参数处理:责任链模式匹配匹配能够处理这类参数的处理器argumentResolvers
    • 返回值处理:责任链模式匹配匹配能够处理这类参数的处理器returnValueHandlers,若是RestFul接口则直接转换为JSON数据返回
  5. ViewResolver根据ModelAndView渲染页面和传递数据到前端(暂未实现,目前仅支持返回JSON数据到前端)

使用方式

该框架与SSM框架的整合方式基本相同,可以参照之前SSM的整合方式来进行整合。

导入依赖

<dependency>
            <groupId>com.duan.summer</groupId>
            <artifactId>summer-context</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.duan</groupId>
            <artifactId>summer-web</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.duan</groupId>
            <artifactId>summer-mybatis</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
  • 上面三个是整合需要的三个基本模块,如果需要AOP,可以再导入AOP模块。

创建数据库实体对象

package org.example.pojo;

/**
 * @author 白日
 * @create 2023/11/9 17:21
 */

public class Employee {
    private Integer id;

    private String name;

    private Integer age;

    private String position;

    //get、set、tostring....
}

Mapper接口(注解实现)

@Component
public interface EmployeeMapper {
    @Insert("INSERT INTO  employee (name, age, position) VALUES (#{name}, #{age}, #{position})")
    int insert(Employee employee);
    @Select("select * from employee where id = #{id}")
    Employee selectByID(@Param("id") Long id);

    @Delete("delete from employee where id = #{id}")
    int deleteByID(@Param("id") Integer id);

    @Update("update employee set name = #{name}, age = #{age}, position = #{position} where id = #{id}")
    int updateByID(Employee employee);
}

同样可支持XML配置文件开发Mapper

Service注入Mapper代理类

@Component
public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;

    public Employee selectById(Long id){
        return employeeMapper.selectByID(id);
    }
}

Controller注入Service层

@Controller
@RequestMapping("employees")
public class EmployeeController {
    @Autowired
    EmployeeService service;
    @RequestMapping(value = "id", requestMethod = RequestType.GET)
    public Employee getEmployeeByID(@RequestParam("id") Long id) {
        return service.selectById(id);
    }
}

配置

  1. 注册数据源到IOC容器
@Configuration
public class JdbcConfig {
    @Bean
    public DataSource dataSource(@Value("${driver}") String driver,
                                 @Value("${url}")  String url,
                                 @Value("${username}") String username,
                                 @Value("${password}") String password){
        DruidDataSource dataSource=new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driver);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}
  1. 配置SqlSessionFactoryBean和MapperScannerConfigurer
@Configuration
public class MyBatisConfig {
    //工厂模式启动SqlSession注册到IOC容器,支持忽略数据库字段前缀和驼峰映射等配置。
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //如果使用XML开发需要在这里设置Mapper包名 sqlSessionFactoryBean.setMapperPackage("mapper");
        return sqlSessionFactoryBean;
    }
    //支持注解开发,在创建Bean之前将Mapper接口的BeanDifination替换为MapperProxyFactory实现将接口替换为代理对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
        //配置mapper扫描路径
        mapperScannerConfigurer.setBasePackage("org.example.mapper");
        return mapperScannerConfigurer;
    }
}

测试

tomcat正常启动后,我们使用PostMan测试该接口是否能够正常使用:
image-20240318105700662
可以看到PostMan正确以JSON格式返回了id为10的employees信息:
image-20240318110017107
再看后端日志输出:

10:58:29.226 [http-nio-8080-exec-8] INFO com.duan.summer.web.DispatcherServlet -- GET /testMVC_war_exploded/employees/id
10:58:29.236 [http-nio-8080-exec-8] DEBUG com.duan.summer.resolve.RequestParamMethodArgumentResolver -- Method getEmployeeByID index 0 param resolve: 10 by resolver RequestParamMethodArgumentResolver//参数处理器
10:58:29.311 [http-nio-8080-exec-8] INFO com.alibaba.druid.pool.DruidDataSource -- {dataSource-1} inited
==>  Preparing:select * from employee where id = ?
==> Parameters: 10(Long), 
<==      Total: 1
//输出日志,模仿了Mybatis框架输出的日志。

我们成功连同了数据库查询到了数据。

使用AOP记录日志

除了IOC,ORM,Web等特性,我们框架还实现了一个AOP的功能,下面我们介绍如何使用我们的AOP来记录日志:

  1. 创建切面类:
@Component
@Aspect
public class LogAspect {
    @Around(targetAnno = LogAnno.class)
    public Object around(ProceedingJoinPoint joinPoint) {
        long begin = System.currentTimeMillis();
        Object proceed = joinPoint.proceed();
        System.out.println("调用" + joinPoint.getMethod().getName() + "方法,耗时:" + (System.currentTimeMillis() - begin));
        return proceed;
    }
}
  • 目前仅支持Aroud类型的通知,并且通知的拦截规则仅支持注解,也就是会为标有LogAnno注解的方法或者类创建代理对象。
  1. 对需要拦截的方法或者类打上LogAnno注解
@LogAnno
public Employee selectById(Long id){
    return employeeMapper.selectByID(id);
}
  1. 配置

我们需要确保LogAspect,切面类能够被扫描到,并且将代理核心类AOPProxyFactory注册到容器中:

@Configuration
public class AopConfig {
    @Bean
    AOPProxyFactory createAroundProxyBeanPostProcessor() {
        return new AOPProxyFactory();
    }
}
  1. 测试
21:50:30.793 [http-nio-8080-exec-8] INFO com.alibaba.druid.pool.DruidDataSource -- {dataSource-1} inited
==>  Preparing:select * from employee where id = ?
==> Parameters: 10(Long), 
<==      Total: 1
调用selectById方法,耗时:317

可以看到,AOP生效成功记录了日志。

teAroundProxyBeanPostProcessor() {
return new AOPProxyFactory();
}
}


4. 测试

```bash
21:50:30.793 [http-nio-8080-exec-8] INFO com.alibaba.druid.pool.DruidDataSource -- {dataSource-1} inited
==>  Preparing:select * from employee where id = ?
==> Parameters: 10(Long), 
<==      Total: 1
调用selectById方法,耗时:317

可以看到,AOP生效成功记录了日志。

  • 54
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值