整个demo分为两个模块:应用模块(com.clf.demo包下);手写mvc框架模块(com.clf.mvcframework包下)
环境:jdk1.8, idea,tomcat8.5
创建项目为maven工程的web项目,创建过程可以参考博客
整个项目的源码链接
首先在mvc模块下对常用的几个注解进行自定义:
在demo中进行常规的应用搭建
实现Controller层
@MyController
@MyRequestMapping("/demo")
public class DemoController {
@MyAutowired
private DemoService demoService;
@MyRequestMapping("/query")
public void query(HttpServletRequest request,
HttpServletResponse response,
@MyRequestParam("/name") String name){
String result = demoService.get(name);
try {
PrintWriter writer = response.getWriter();
writer.write(result);
if (writer != null){
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
实现Service层:
@MyService
public class DemoServiceImpl implements DemoService {
@Override
public String get(String name) {
return "success, My name is " + name;
}
}
然后实现自定义的DispatcherServlet实现配置信息的加载,容器的初始化和依赖的注入等功能。
Web Application
clfmvc
com.clf.mvcframework.servlet.ClfDispatcherServlet
contextConfigLocation
application.properties
1
clfmvc
/*
4.0.0
com.clf.mvcframework
MySpringMVC
1.0-SNAPSHOT
UTF-8
1.8
1.8
1.8
javax.servlet
servlet-api
2.5
在resource文件夹下创建application.properties文件
添加应用扫描的包路径,我的整个应用模块在demo包下(具体根据自己的项目结构进行创建):
首先创建一个自定义的DispatcherServlet继承HttpServlet,然后实现init()方法。
初始化阶段主要包括五个流程:
1.加载配置文件
2.解析配置文件,扫描应用相关的类
3.初始化所有相关的类,并保存到IOC容器中
4.完成自动化的依赖注入,DI
5.创建HandlerMapping将url和method建立对应的关系
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("======================SpringMVC is initializing=====================");
/**
* 1.加载配置文件
*/
System.out.println("获取初始参数:" + config.getInitParameter("contextConfigLocation"));
doLoadConfig(config.getInitParameter("contextConfigLocation"));
/**
* 2.解析配置文件,扫描所有相关的类
*/
//获取到properties中的scanPackage=com.clf.demo
doScanner(contextConfig.getProperty("scanPackage"));
/**
* 3.初始化所有相关的类.并且保存到IOC容器中
*/
doInstance();
/**
* 4.完成自动化的依赖注入,DI
*/
doAutowired();
/**
* 5.创建HandlerMapping将url和method建立对应关系
*/
initHandlerMapping();
}
根据初始化参数获取对应properties文件路径
private void doLoadConfig(String contextConfigLocation) {
//从类路径下去取得properties
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
//加载所有配置信息
contextConfig.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过扫描包路径将包下所有的类文件的className存入list集合中,便于后面对类进行遍历读取类名进行bean的实例化
private void doScanner(String scanPackage) {
//将 com.clf.demo 格式转换成 /com/clf/demo
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replace(".", "/"));
//拿到文件目录
File classDir = new File(url.getFile());
for (File file : classDir.listFiles()) {
//目录下可能还有多级子包,需要进行递归判断
if (file.isDirectory()){
//如果是子文件夹,则进行继续扫描
doScanner(scanPackage + "." + file.getName());
}else {
if (!file.getName().endsWith(".class")){
continue;
}
//如果拿到的是一个文件,则将对应的路径放到容器中
String className = (scanPackage + "." + file.getName().replace(".class", "").trim());
classNames.add(className);
}
}
}
扫描自定义的注解进行bean的实例化并存放到ioc容器中
private void doInstance() {
//如果list为空, 则没有扫描到任何东西,直接返回
if (classNames.isEmpty()){ return; }
try {
//对扫描到的类进行反射
for (String className : classNames) {
//根据className获取到对应的类
Class> clazz = Class.forName(className);
//并不是所有的手写spring类都需要反射,只对加入自定义注解的进行反射
if (clazz.isAnnotationPresent(MyController.class)){
//获取类的类名,而不包括包名,同时将获取到的类名首字母小写
String beanName = lowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, clazz.newInstance());
}else if (clazz.isAnnotationPresent(MyService.class)){
//1.类名首字母小写
//2.自定义命名优先
MyService service = clazz.getAnnotation(MyService.class);
String beanName = service.value();
//判断是否有自定义beanName
if ("".equals(beanName)){
//没有自定义的命名就使用类名首字母小写
beanName = lowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3.用接口的全称作为key, 用接口的实现类的实例作为值
Class>[] interfaces = clazz.getInterfaces();
for (Class> i : interfaces) {
//如果存在一个接口的多个实例抛出异常
if (ioc.containsKey(i.getName())){
throw new Exception("The beanName is exists");
}
ioc.put(i.getName(), instance);
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
对每个bean的成员属性进行注入,即使用了@Autowired的成员变量
private void doAutowired() {
if (ioc.isEmpty()){ return; }
for (Map.Entry entry : ioc.entrySet()) {
//获取当前类的所有的属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
//遍历属性
for (Field field : fields) {
//判断是否是需要进行依赖注入的属性
if (!field.isAnnotationPresent(MyAutowired.class)){ return; }
MyAutowired autowired = field.getAnnotation(MyAutowired.class);
//获取自定的注入的beanName
String beanName = autowired.value();
if ("".equals(beanName)){
//获取到接口的全称
beanName = field.getType().getName();
}
//强制赋值
field.setAccessible(true);
try {
//从ioc获取实例进行注入
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
将每一个url和需要访问的方法进行映射存入handlerMapping中,保证保证在进行请求url时容器可以根据请求的ur获取Method进行反射调用方法。
private void initHandlerMapping() {
if (ioc.isEmpty()){ return; }
for (Map.Entry entry : ioc.entrySet()) {
Class> clazz = entry.getValue().getClass();
//如果不是一个Controller直接跳过
if (!clazz.isAnnotationPresent(MyController.class)){ continue; }
String baseUrl = "";
//判断是否拥有RequestMapping注解
if (clazz.isAnnotationPresent(MyRequestMapping.class)){
MyRequestMapping requestMapping = clazz.getAnnotation(MyRequestMapping.class);
//获取Controller上的baseUrl
baseUrl = requestMapping.value();
}
Method[] methods = clazz.getMethods();
//遍历类的方法
for (Method method : methods) {
//如果没有MyRequestMapping直接跳过
if (!method.isAnnotationPresent(MyRequestMapping.class)) { continue; }
MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
//获取完整的url,同时使用正则将多个 "/" 替换成只有一个
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
handlerMapping.put(url, method);
System.out.println("Mapped: " + url + "," + method);
}
}
}
运行的主要流程:
1.前端根据url发送请求
2.请求后后端通过自定义的DispatcherServlet获取request
3.通过request获取当前访问的url
4.handlerMapping实际上就是一个Map,分别存放url和对应的Method方法
5.通过获取method对象来获取对应的Class,再由Class名称获取原始的beanName,通过ioc容器由beanName获取bean对象进行注入
/**
* 自定义一个HandlerMapping
*/
private Map handlerMapping = new HashMap();
6.将方法返回的信息写入response
启动成功:
访问不存在的url
请求url:/demo/query