通过阅读SSM框架相关书籍,文章,源码从而仿照了一个实现了大部分功能的SSM框架。通过学习这个框架
Summer 框架是一个仿真的轻量级 JavaEE 开发框架,旨在模拟实现了 Spring、SpringMVC和 MyBatis 的核心功能,提供了IOC、AOP、半ORM、Web 开发支持等特性,基本实现了SSM的主要功能。
这是项目的核心模块,在这个模块实现了一个简单的IOC容器。在这里我们没有实现过时的XML配置而仅通过注解配置来实现Bean的注册,除此之外IOC容器还支持Yaml
和XML
格式的配置信息读取、BeanPostProcessor
、BeanFactoryPostProcessor、Aware等扩展机制。
过程文档:
基于JDK和bytebuddy的动态代理技术和IOC提供的BeanPostProcessor和Aware机制实现的AOP。使用了责任链模式和回溯算法解决了一个PointCut对应多个Advice的调用逻辑。
过程文档:
基于JDK动态代理机制的半ORM框架,支持XML配置文件和注解开发。并且基于IOC的BeanFactoryPostProcessor等扩展机制实现了与IOC模块的整合。
过程文档:
基于Servlet实现的Web框架。通过实现Servlet提供的ServletContainerInitializer接口加载载父子容器和DispatcherServlet,并使用FactoryBean机制初始化DispatcherServlet的各种组件如HandlerMapping、HandlerMappingAdapter等。
过程文档:
- SpringMVC与Spring是如何联系的
- .....(没写完)
框架启动的过程总结起来就是:
启动Tomcat时使用ServletContainerInitializer接口加载IOC父子容器和DispatcherServlet。
下面我们来分析一下启动的详细流程:
- 启动
Tomcat
时调用WebApplicationInitializer
实现类的onStartup
- 在
onStartup
方法中向ServletContext
容器中注册一个监听器ContextLoaderInitializer
和持有子容器的DispatcherServlet
ContextLoaderInitializer
和DispatcherServlet
组件的初始化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();
要处理请求,我们主要需要解决下面的问题:
- 如何保存处理请求的
Handler
,如何匹配对应的Handler
?
我们使用一个组件HandlerMapping
,解析Controller
中的全部Handler
,并按照一定规则去匹配Handler。
Handler
:Controller
里面的@RequestMapping
标志的每一个方法都是一个Handler
- 如何处理参数和返回值
因为Handler
包含的只是Controller
方法的一些信息,并不能直接对请求进行处理,因此我们将通过一个Adapter
来根据Handler
的信息和Request
、Response
请求信息进行参数的处理调用以及返回值的处理。
下面是处理的流程,与SpringMVC基本相似:
- 前端的所有请求都被一个
Servlet
也就是DispatcherServlet
拦截。 - 所有类型的
Request
都由DispatcherServlet
的doService
方法来进行处理。 - 通过
handlerMapping
组件来根据Request
的URL
来匹配对应的Handler
(包括拦截器)。 - 根据获取的
Handler
和Request
、Response
构建handlerAdapter
拿到ModelAndView
,这里又包含了参数的处理和返回值的处理- 参数处理:责任链模式匹配匹配能够处理这类参数的处理器
argumentResolvers
。 - 返回值处理:责任链模式匹配匹配能够处理这类参数的处理器
returnValueHandlers
,若是RestFul接口则直接转换为JSON数据返回
- 参数处理:责任链模式匹配匹配能够处理这类参数的处理器
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....
}
@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
@Component
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee selectById(Long id){
return employeeMapper.selectByID(id);
}
}
@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);
}
}
- 注册数据源到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;
}
}
- 配置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测试该接口是否能够正常使用:
可以看到PostMan正确以JSON格式返回了id为10的employees信息:
再看后端日志输出:
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框架输出的日志。
我们成功连同了数据库查询到了数据。
除了IOC,ORM,Web等特性,我们框架还实现了一个AOP的功能,下面我们介绍如何使用我们的AOP来记录日志:
- 创建切面类:
@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
注解的方法或者类创建代理对象。
- 对需要拦截的方法或者类打上
LogAnno
注解
@LogAnno
public Employee selectById(Long id){
return employeeMapper.selectByID(id);
}
- 配置
我们需要确保LogAspect
,切面类能够被扫描到,并且将代理核心类AOPProxyFactory
注册到容器中:
@Configuration
public class AopConfig {
@Bean
AOPProxyFactory createAroundProxyBeanPostProcessor() {
return new AOPProxyFactory();
}
}
- 测试
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生效成功记录了日志。