目录
简介
300行代码手写Spring(包含IOC、DI、MVC部分),提炼Spring精华设计思想,并保证功能可用。
目的要达成的效果是,手写的Spring能在初始化时完成相关类的实例化并保存到容器中,并对这些实例对象的属性进行注入,同时将URL与Method建立映射;这些工作将保证运行时,前端的请求可以被测试的controller正常处理并返回结果。
PIG版Spring实现的基本思路如下图——
配置阶段
web.xml中指定servlet-class、init-param等关键配置:
<?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>Pig Web Application</display-name>
<servlet>
<servlet-name>pigmvc</servlet-name>
<servlet-class>com.zhu.mvcframework.servlet.PIGDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>pigmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
自定义注解类:
如 PIGController,模拟Spring中的Controller注解:
/**
* 页面交互
*
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PIGController {
String value() default "";
}
编写 PIGDispatcherServlet
用户请求到达前端控制器,它就相当于 mvc 模式中的 c。dispatcherServlet 是整个springMVC流程控制的中心,由它调用其它组件处理用户的请求。
当我们要自己实现一个dispatcherServlet 时,可以先搭好骨架,再填空。关键是ioc容器(一个Map)。
public class PIGDispatcherServlet extends HttpServlet {
// 初始化ioc容器
private Map<String,Object> ioc = new HashMap<String,Object>();
@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 {
// 6. 根据URL完成方法的调度
doDispatch(req, resp);
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
}
// 初始化阶段
@Override
public void init() throws ServletException {
// ================== IoC =======================
// 1. 加载配置文件
doLoadConfig();
// 2. 扫描相关类
doScanner();
// 3. 初始化扫描到的类,并创建实例保存到ioc容器中
doInstance();
// ================== DI =======================
// 4. 完成DI依赖注入,自动赋值
doAutowired();
// ================== MVC =======================
// 5. 初始化HandlerMapping
doInitHandlerMapping();
System.out.println("PIG Spring framework is initialized!");
}
private void doInitHandlerMapping() {
}
private void doAutowired() {
}
private void doInstance() {
}
private void doScanner() {
}
private void doLoadConfig() {
}
接下来关注初始化阶段所要做的事情。
1.doLoadConfig
这一步就是去加载配置文件。
// 1. 加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
首先要拿到spring主配置文件的配置(路径),这个需要到web.xml中去取(见下图)
我们需要通过param-name,来获取param-value。这个值就是我们想要的配置的路径。
方法体如下:
private Properties contextConfig = new Properties();
private void doLoadConfig(String contextConfigLocation) {
// 直接从ClassPath下找spring的配置文件
// 相当于把application.properties文件读到了内存中(contextConfig这个对象)
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2.doScanner()
这一步就是去扫描相关的类。这些类我们将在application.properties中注册如下:
因此首先将注册的包名取到:
// 2. 扫描相关类
doScanner(contextConfig.getProperty("scanPackage"));
具体方法如下:
// 扫描到的类名
private List<String> classNames = new ArrayList<String>();
private void doScanner(String scanPackage) {
// 到ClassPath下找相关的.class文件
// scanPackage包路径对应一个文件夹
// 下面转为文件路径
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classpath = new File(url.getFile());
for (File file : classpath.listFiles()) {
// 如果是文件夹,则递归遍历之
if (file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
} else {
if (!file.getName().endsWith(".class")) {continue;}
String className = scanPackage + "." + file.getName().replaceAll(".class", "");
// 可以在实例化阶段通过调用Class.forName(className)反射创建实例
classNames.add(className);
}
}
}
关键步骤就是将扫描到的类名存到List中。
3.doInstance()
这一步中将扫描到的类进行实例化,并将实例化后的对象存到ioc容器中。
private void doInstance() {
if (classNames.isEmpty()) {
return;
}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
// 加了注解的才能实例化
// 使用了Controller注解的情况
if (clazz.isAnnotationPresent(PIGController.class)) {
// 首字母小写
// getSimpleName()是无包名的
String beanName = toLowerFirstCase(clazz.getSimpleName());
Object instance = clazz.newInstance();
ioc.put(beanName,instance);
} else if (clazz.isAnnotationPresent(PIGService.class)) {
// 使用了Service注解的情况
// 1.默认类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
// 2.自定义beanName,保证唯一(处理不同包下类名相同的情况)
// 拿到注解的值,也就是自定义的beanName
PIGService service = clazz.getAnnotation(PIGService.class);
if (!"".equals(service.value())) {
beanName = service.value();
}
Object instance = clazz.newInstance();
ioc.put(beanName,instance);
// 3.用全类名作为beanName(如果需要注入的类型是一个接口,那么就需要用全类名找到其实现类)
for (Class<?> i : clazz.getInterfaces()) {
// 若一个接口被多个类实现
if (ioc.containsKey(i.getName())) {
throw new Exception("The beanName is exits !")
}
ioc.put(i.getName(), instance);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
4.doAutowired()
上一步已经将类进行了实例化,这一步将实现依赖注入。
private void doAutowired() {
if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
// 依次拿到IOC容器中实例的所有字段(属性)
// 只能拿到public的属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
// 如果没有加注解则跳过
if (!field.isAnnotationPresent(PIGAutowired.class)) {
continue;
}
PIGAutowired autowired = field.getAnnotation(PIGAutowired.class);
String beanName = autowired.value().trim();
// 若注解未指定值,则取其类型作为beanName
if ("".equals(beanName)) {
beanName = field.getType().getName();
}
// 暴力访问,保证所有访问类型都可注入成功
field.setAccessible(true);
try {
// 以下举例说明
// field相当于@PIGAutowired private IDemoService demoService (为DemoAction类中的一个字段)
// entry.getValue()相当于DemoAction的实例
// ioc.get(beanName)相当于从ioc容器中拿到key为IDemoService(全类名)对应的实例,即demoService的实例
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
5.doInitHandlerMapping()
这一步要做的事是初始化HandlerMapping,HandlerMapping 负责根据用户请求找到 对应的Handler (处理器),也就是针对加了"Controller"注解的类,让程序能调用Controller中的方法。
// 保存RequestMapping映射关系
private Map<String, Method> handlerMapping = new HashMap<String, Method>();
private void doInitHandlerMapping() {
if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
// 只针对加了Controller注解的类
if (!clazz.isAnnotationPresent(PIGController.class)) {
continue;
}
// 处理类上的url
String baseUrl = "";
if (clazz.isAnnotationPresent(PIGRequestMapping.class)) {
PIGRequestMapping requestMapping = clazz.getAnnotation(PIGRequestMapping.class);
baseUrl = requestMapping.value();
}
// 默认只获取public方法
for (Method method : clazz.getMethods()) {
// 只处理加了PIGRequestMapping注解的方法
if (!method.isAnnotationPresent(PIGRequestMapping.class)) {
continue;
}
// 拼接URL
PIGRequestMapping requestMapping = method.getAnnotation(PIGRequestMapping.class);
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/");
// 存入map中
handlerMapping.put(url, method);
System.out.println("Mapped " + url +"," + method);
}
}
}
6.doDispatch()
以上部分完成了初始化阶段的任务,接下来关注运行阶段。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 获取URI:统一资源标识符 如/HttpStudy/demo1
String url = req.getRequestURI();
// 获取虚拟目录 如/HttpStudy
String contextPath = req.getContextPath();
// 将 /HttpStudy/demo1 替换为 /demo1
url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
if (!this.handlerMapping.containsKey(url)) {
resp.getWriter().write("404 Not Found !!");
return;
}
Method method = this.handlerMapping.get(url);
// 获取请求中所有参数的map集合
Map<String,String[]> paramsMap = req.getParameterMap();
// 拿到方法的形参列表
Class<?>[] parameterTypes = method.getParameterTypes();
// 声明实参列表
Object[] paramValues = new Object[parameterTypes.length];
// 根据形参列表的配置 给实参列表赋值
for (int i = 0; i < parameterTypes.length; i++) {
Class paramterType = parameterTypes[i];
if (paramterType == HttpServletRequest.class) {
paramValues[i] = req;
continue;
} else if (paramterType == HttpServletResponse.class) {
paramValues[i] = resp;
continue;
} else if (paramterType == String.class) {
// 二维数组:一个方法有多个注解,一个注解有多个值
Annotation[][] pa = method.getParameterAnnotations();
for (int j = 0; j < pa.length; j++) {
for (Annotation annotation : pa[i]) {
if (annotation instanceof PIGRequestParam) {
// 从注解配置中取出参数名称
String paramName = ((PIGRequestParam) annotation).value();
if (!"".equals(paramName.trim())) {
// 从请求体中取出参数的值
String value = Arrays.toString(paramsMap.get(paramName))
.replaceAll("\\[|\\]","")
.replaceAll("\\s","");
paramValues[i] = value;
}
}
}
}
}
}
// 投机取巧,获取实例对象
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
// 反射调用方法
// 第一个参数,Method所在的实例
// 第二个参数,Method的实参列表
method.invoke(ioc.get(beanName), new Object[]{req, resp, paramValues});
}
测试
写一个简单的Controller:
/**
* 公布接口url
*
*/
@PIGController
@PIGRequestMapping("/demo")
public class MyAction {
@PIGAutowired
private IDemoService demoService;
private IDemoService demoService2;
@PIGRequestMapping("/query")
public void query(HttpServletRequest request, HttpServletResponse response,
@PIGRequestParam("name") String name, @PIGRequestParam("id") String id) {
String result = "My name is " + name + ", id = " + id;
out(response,result);
}
private void out(HttpServletResponse resp,String str){
try {
resp.getWriter().write(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
将代码部署到tomcat即可运行并测试:
启动tomcat,使用调试模式能看到初始化之后的状态:
随后发起请求:
http://localhost:8080/pig_spring/demo/query?name=zhuzhu&id=007
成功返回:
源码
https://github.com/MagicalPiggy/pig-spring/tree/master/pig-spring-1.0