实现思路
先来介绍一下 Mini 版本的 Spring 基本实现思路,如下图所示:
自定义配置
配置 application.properties 文件
scanPackage=com.test.demo
配置 web.xml 文件
大家都知道,所有依赖于 web 容器的项目,都是从读取 web.xml 文件开始的。我们先配置好 web.xml 中的内容。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Web Application</display-name>
<servlet>
<servlet-name>mymvc</servlet-name>
<servlet-class>com.gupaoedu.mvcframework.v2.servlet.GPDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mymvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
自定义 Annotation
MyAutowired
package com.my.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAutowired {
String value() default "";
}
MyController
package com.my.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
String value() default "";
}
MyRequestMapping
package com.my.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "";
}
MyService
package com.my.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyService {
String value() default "";
}
配置 Annotation
配置业务实现类 DemoService:
package com.my.demo.mvc.service.impl;
import com.gupaoedu.demo.service.IDemoService;
import com.my.mvcframework.annotation.MyService;
/**
* 核心业务逻辑
*/
@MyService
public class DemoService implements IDemoService{
@Override
public String get(String name) {
return "My name is " + name;
}
}
配置请求入口类 DemoAction:
package com.my.demo.mvc.action;
import com.my.demo.mvc.service.IDemoService;
import com.my.mvcframework.annotation.MyAutowired;
import com.my.mvcframework.annotation.MyController;
import com.my.mvcframework.annotation.MyRequestMapping;
import com.my.mvcframework.annotation.MyRequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@MyController
@MyRequestMapping("/demo")
public class DemoAction {
@MyAutowired
private IDemoService iDemoService;
@MyRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@MyRequestParam("name") String name){
// String result = demoService.get(name);
String result = "My name is " + name;
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@MyRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@MyRequestParam("a") Integer a, @MyRequestParam("b") Integer b){
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
@MyRequestMapping("/remove")
public void remove(HttpServletRequest req,HttpServletResponse resp,
@MyRequestParam("id") Integer id){
}
}
至此,配置阶段就已经完成。
容器初始化
采用了常用的设计模式(工厂模式、单例模式、委派模式、策略模式),将 init()方法中的代
码进行封装。按照之前的实现思路,先搭基础框架,再填肉注血,具体代码如下:
package com.my.mvcframework.v1.servlet;
import com.my.mvcframework.annotation.MyAutowired;
import com.my.mvcframework.annotation.MyController;
import com.my.mvcframework.annotation.MyRequestMapping;
import com.my.mvcframework.annotation.MyService;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
/**
* @Author: wangy
* @Date: 2020/12/14 14:38
*/
public class MyDispatcherServlet extends HttpServlet {
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描的所有的类名
private List<String> classNames = new ArrayList<String>();
//传说中的IOC容器,我们来揭开它的神秘面纱
//为了简化程序,暂时不考虑ConcurrentHashMap
// 主要还是关注设计思想和原理
private Map<String,Object> ioc = new HashMap<String,Object>();
//保存url和Method的对应关系
private Map<String, Method> handlerMapping = new HashMap<String,Method>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req,resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//绝对路径
String url = req.getRequestURI();
//处理成相对路径
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
if(!this.handlerMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!!!");
return;
}
Method method = this.handlerMapping.get(url);
//投机取巧的方式
//通过反射拿到method所在class,拿到class之后还是拿到class的名称
//再调用toLowerFirstCase获得beanName
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
//为了投机取巧,暂时写死
Map<String,String[]> params = req.getParameterMap();
//获取方法的形参列表
Class<?> [] parameterTypes = method.getParameterTypes();
method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
}
@Override
public void init(ServletConfig config) throws ServletException {
//1、加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2、扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3、初始化扫描到的类,并且将它们放入到ICO容器之中
doInstance();
//4、完成依赖注入
doAutowired();
//5、初始化HandlerMapping
initHandlerMapping();
System.out.println("My Spring framework is init.");
}
/**初始化HandlerMapping*/
private void initHandlerMapping() {
if(ioc.isEmpty()){ return; }
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(MyController.class)){continue;}
//保存写在类上面的@GPRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(MyRequestMapping.class)){
MyRequestMapping requestMapping = clazz.getAnnotation(MyRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for (Method method : clazz.getMethods()) {
if(!method.isAnnotationPresent(MyRequestMapping.class)){continue;}
MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
//优化
// //demo///query
String url = ("/" + baseUrl + "/" + requestMapping.value())
.replaceAll("/+","/");
handlerMapping.put(url,method);
System.out.println("Mapped :" + url + "," + method);
}
}
}
private void doAutowired() {
if(ioc.isEmpty()){return;}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
//Declared 所有的,特定的 字段,包括private/protected/default
//正常来说,普通的OOP编程只能拿到public的属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if(!field.isAnnotationPresent(MyAutowired.class)){continue;}
MyAutowired autowired = field.getAnnotation(MyAutowired.class);
//如果用户没有自定义beanName,默认就根据类型注入
//这个地方省去了对类名首字母小写的情况的判断,这个作为课后作业
//小伙伴们自己去完善
String beanName = autowired.value().trim();
if("".equals(beanName)){
//获得接口的类型,作为key待会拿这个key到ioc容器中去取值
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//反射中叫做暴力访问, 强吻
field.setAccessible(true);
try {
//用反射机制,动态给字段赋值
field.set(entry.getValue(),ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**初始化扫描到的类,并且将它们放入到ICO容器之中*/
private void doInstance() {
//初始化,为DI做准备
if(classNames.isEmpty()){
return;
}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
//什么样的类才需要初始化呢?
//加了注解的类,才初始化,怎么判断?
//为了简化代码逻辑,主要体会设计思想,只举例 @Controller和@Service,
// @Componment...就一一举例了
if(clazz.isAnnotationPresent(MyController.class)){
Object instance = clazz.newInstance();
//Spring默认类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance);
}else if(clazz.isAnnotationPresent(MyService.class)){
//1、自定义的beanName
MyService service = clazz.getAnnotation(MyService.class);
String beanName = service.value();
//2、默认类名首字母小写
if("".equals(beanName.trim())){
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName,instance);
//3、根据类型自动赋值,投机取巧的方式
for (Class<?> i : clazz.getInterfaces()) {
if(ioc.containsKey(i.getName())){
throw new Exception("The “" + i.getName() + "” is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(),instance);
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
//如果类名本身是小写字母,确实会出问题
//但是我要说明的是:这个方法是我自己用,private的
//传值也是自己传,类也都遵循了驼峰命名法
//默认传入的值,存在首字母小写的情况,也不可能出现非字母的情况
//为了简化程序逻辑,就不做其他判断了,大家了解就OK
//其实用写注释的时间都能够把逻辑写完了
private String toLowerFirstCase(String simpleName) {
char [] chars = simpleName.toCharArray();
//之所以加,是因为大小写字母的ASCII码相差32,
// 而且大写字母的ASCII码要小于小写字母的ASCII码
//在Java中,对char做算学运算,实际上就是对ASCII码做算学运算
chars[0] += 32;
return String.valueOf(chars);
}
/**加载配置文件*/
private void doLoadConfig(String contextConfigLocation){
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.my.demo 从文件中保存到了内存中
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();
}
}
}
}
/**扫描相关的类*/
private void doScanner(String scanPackage){
//scanPackage = com.my.demo ,存储的是包路径
//转换为文件路径,实际上就是把.替换为/就OK了
//classpath
URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\.","/"));
String fileString = url.getFile();
File file = new File(fileString);
for (File o :file.listFiles()) {
if(o.isDirectory()){
doScanner(scanPackage + "."+o.getName());
}else{
if(!file.getName().endsWith(".class")){ continue;}
String className = (scanPackage + "." + file.getName().replace(".class",""));
classNames.add(className);
}
}
}
}