最近在看spring源码,纸上得来终觉浅,觉知此事要躬行,先根据基本思路写一个mini版本,后续有时间再一点点丰富,慢慢深入理解,markdown一下。
先介绍下mini版本的spring基本思路:
自定义配置:
配置 application.properties 文件
为了解析方便,我们用 application.properties 来代替 application.xml 文件,具体配置内容如下:
scanPackage=com.dyx.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>dyx Web Application</display-name>
<servlet>
<servlet-name>dyxmvc</servlet-name>
<servlet-class>com.dyx.mvcframework.v2.servlet.DYXDispatcherServlet</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>dyxmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
自定义 Annotation :
@DYXService 注解:
package com.dyx.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DYXService {
String value() default "";
}
@DYXAutowired 注解:
package com.dyx.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DYXAutowired {
String value() default "";
}
@DYXController 注解:
package com.dyx.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DYXController {
String value() default "";
}
@DYXRequestMapping 注解:
package com.dyx.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DYXRequestMapping {
String value() default "";
}
@DYXRequestParam 注解:
package com.dyx.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DYXRequestParam {
String value() default "";
}
配置 Annotation :
配置业务实现类 DemoService:
package com.dyx.demo.service.impl;
import com.dyx.mvcframework.annotation.DYXService;
import com.dyx.demo.service.IDemoService;
/**
* 核心业务逻辑
*/
@DYXService
public class DemoService implements IDemoService{
public String get(String name) {
return "My name is " + name;
}
}
配置请求入口类 DemoAction:
package com.dyx.demo.mvc.action;
import com.dyx.demo.service.IDemoService;
import com.dyx.mvcframework.annotation.DYXAutowired;
import com.dyx.mvcframework.annotation.DYXController;
import com.dyx.mvcframework.annotation.DYXRequestMapping;
import com.dyx.mvcframework.annotation.DYXRequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//虽然,用法一样,但是没有功能
@DYXController
@DYXRequestMapping("/demo")
public class DemoAction {
@DYXAutowired
private IDemoService demoService;
@DYXRequestMapping("/query.*")
public void query(HttpServletRequest req, HttpServletResponse resp,
@DYXRequestParam("name") String name){
// String result = demoService.get(name);
String result = "My name is " + name;
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
容器初始化
基本框架如下:
//初始化
public void init(ServletConfig config) throws ServletException{
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//扫描相关类
doScanner(contextConfig.getProperty("scanPackage"));
//初始化扫描到的类,并且将他们放到ioc容器中
doInstance();
//完成依赖注入
doAutowired();
//初始化handlerMapping
initHandlerMapping();
System.out.println("spring init is end");
}
声明全局的成员变量:
//保存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 List<Handler> handlerMapping = new ArrayList();
实现 doLoadConfig()方法加载配置文件:
//加载文件配置
private void doLoadConfig(String contextConfigLocation){
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相当于从文件中保存到了内存中
InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try{
contextConfig.load(fis);
} catch (Exception e){
e.printStackTrace();
}finally{
if(null != fis){
try{
fis.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
}
实现 doScanner()方法扫描相关类:
//扫描相关的类
private void doScanner(String scanPackage){
//scanPackage = com.dyx.demo ,存储的是包路径
//转换为文件路径,实际上就是把.替换为/就OK了
//classpath
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().
replace(".class",""));
classNames.add(className);
}
}
}
实现 doInstance()方法将扫描到的类放入到ICO容器之中
/**
* 初始化扫描到的类放入到ICO容器之中
*/
private void doInstance(){
//如果没有扫描的类则直接返回
if(classNames.isEmpty()){return;}
try{
for (String className :classNames) {
Class<?> clazz = Class.forName(className);
//初始化加注解的类,这里只初始化@Controller和@Service
if(clazz.isAnnotationPresent(DYXController.class)){
Object instance = (Object) clazz.newInstance();
//spring类首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance);
} else if(clazz.isAnnotationPresent(DYXService.class)){
//自定义的beanName
DYXService service = clazz.getAnnotation(DYXService.class);
String beanName = service.value();
//默认首字母小写
if("".equals(beanName.trim())){
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName,instance);
//根据类型自动赋值,采用投机取巧的方式
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();
}
}
为了处理方便,自己实现了 toLowerFirstCase 方法,来实现类名首字母小写,具体代码如下:
/**
* 如果类名本身是小写字母,确实会出问题
* 传值也是自己传,类也都遵循了驼峰命名法
* 默认传入的值,存在首字母小写的情况,也不可能出现非字母的情况
* 为了简化程序逻辑,就不做其他判断了,大家了解就OK
*/
private String toLowerFirstCase(String simpleName){
char[] chars = simpleName.toCharArray();
//Java中,对char做算学运算,实际上就是对ASCII码做算学运算,大写字母的ASCII码要小于小写字母的ASCII码,相差32
chars[0] += 32;
return String.valueOf(chars);
}
实现 doAutowired()方法自动依赖注入
/**
*自动依赖注入
*/
private void doAutowired(){
//如果ioc容器为空则直接返回
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(DYXAutowired.class)){continue;}
DYXAutowired autowired = field.getAnnotation(DYXAutowired.class);
//如果用户没有自定义beanName,默认就根据类型注入(待补充对首字母小写的判断)
String beanName = autowired.value().trim();
//如果beanName 为空则用接口类型去ioc容器中获取
if("".equals(beanName)){
beanName =field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//反射中叫做暴力访问, 强吻
field.setAccessible(true);
//用反射机制,动态给字段赋值
try {
field.set(entry.getValue(),ioc.get(beanName));
}catch (IllegalAccessException e){
e.printStackTrace();
}
}
}
}
实现 initHandlerMapping()方法 ,初始化url和Method的一对一对应关系
/**
* 初始化url和Method的一对一对应关系
*/
private void initHandlerMapping(){
//如果ioc容器为空则直接返回
if(ioc.isEmpty()){return;}
//循环处理
for (Map.Entry<String,Object> entry:ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
//不是controller则退出
if(!clazz.isAnnotationPresent(DYXController.class)){continue;}
//保存写在类上面的@DYXRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(DYXRequestMapping.class)){
DYXRequestMapping requestMapping = clazz.getAnnotation(DYXRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for (Method method:clazz.getMethods()) {
if(!method.isAnnotationPresent(DYXRequestMapping.class)){continue;}
DYXRequestMapping requestMapping = method.getAnnotation(DYXRequestMapping.class);
//正则
String regex =("/"+baseUrl+"/"+requestMapping.value())
.replaceAll("/+","/");
Pattern pattern = Pattern.compile(regex);
this.handlerMapping.add(new Handler(pattern,entry.getValue(),method));
System.out.println("Mapped:"+pattern+","+method);
}
}
}
到这里位置初始化阶段就已经完成,接下实现运行阶段的逻辑
运行阶段 :
doPost/doGet 的代码
@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 Exception,Detail:"+Arrays.toString(e.getStackTrace()));
}
}
doPost()方法中,的具体逻辑在 doDispatch()方法中:
其中为了支持正则添加getHandler()方法,为了支持强制类型转换加了convert()方法。
//主调度器
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
Handler handler =getHandler(req);
if(handler == null ){
resp.getWriter().write("404 Not Found!!!");
return;
}
//获取形参列表
Class<?> [] paramTypes = handler.getParamTypes();
java.lang.Object[] paramValues = new java.lang.Object[paramTypes.length];
Map<String,String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> parm : params.entrySet()) {
String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","")
.replaceAll("\\s",",");
if(!handler.paramIndexMapping.containsKey(parm.getKey())){continue;}
int index = handler.paramIndexMapping.get(parm.getKey());
paramValues[index] = convert(paramTypes[index],value);
}
if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
java.lang.Object returnValue = handler.method.invoke(handler.controller,paramValues);
if(returnValue == null || returnValue instanceof Void){ return; }
resp.getWriter().write(returnValue.toString());
}
//获取hander
private Handler getHandler(HttpServletRequest req){
if(handlerMapping.isEmpty()){ return null; }
//绝对路径
String url = req.getRequestURI();
//处理成相对路径
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
for(Handler handler :this.handlerMapping){
Matcher matcher = handler.getPattern().matcher(url);
if(!matcher.matches()){continue;}
return handler;
}
return null;
}
//url传过来的参数都是String类型的,HTTP是基于字符串协议
//只需要把String转换为任意类型就好
private java.lang.Object convert(Class<?> type, String value){
//如果是int
if(Integer.class == type){
return Integer.valueOf(value);
}
else if(Double.class == type){
return Double.valueOf(value);
}
//如果还有double或者其他类型,继续加if
//这时候,我们应该想到策略模式了
//在这里暂时不实现,希望小伙伴自己来实现
return value;
}
完整的pom文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>dyx-spring-1.0</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<!-- dependency versions -->
<servlet.api.version>2.4</servlet.api.version>
</properties>
<dependencies>
<!-- requied start -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet.api.version}</version>
<scope>provided</scope>
</dependency>
<!-- requied end -->
</dependencies>
<build>
<finalName>${artifactId}</finalName>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.class</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<verbose />
<bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<executions>
<execution>
<id>copy-resources</id>
<!-- here the phase you need -->
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.26</version>
<configuration>
<webDefaultXml>src/main/resources/webdefault.xml</webDefaultXml>
<contextPath>/</contextPath>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>8080</port>
</connector>
</connectors>
<scanIntervalSeconds>0</scanIntervalSeconds>
<scanTargetPatterns>
<scanTargetPattern>
<directory>src/main/webapp</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</scanTargetPattern>
</scanTargetPatterns>
<systemProperties>
<systemProperty>
<name>
javax.xml.parsers.DocumentBuilderFactory
</name>
<value>
com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
</value>
</systemProperty>
<systemProperty>
<name>
javax.xml.parsers.SAXParserFactory
</name>
<value>
com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl
</value>
</systemProperty>
<systemProperty>
<name>
javax.xml.transform.TransformerFactory
</name>
<value>
com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
</value>
</systemProperty>
<systemProperty>
<name>org.eclipse.jetty.util.URI.charset</name>
<value>UTF-8</value>
</systemProperty>
</systemProperties>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
<webResources>
<resource>
<!-- this is relative to the pom.xml directory -->
<directory>src/main/resources/</directory>
<targetPath>WEB-INF/classes</targetPath>
<includes>
<include>**/*.*</include>
</includes>
<!-- <excludes>
<exclude>**/local</exclude>
<exclude>**/test</exclude>
<exclude>**/product</exclude>
</excludes> -->
<filtering>true</filtering>
</resource>
<resource>
<!-- this is relative to the pom.xml directory -->
<directory>src/main/resources</directory>
<targetPath>WEB-INF/classes</targetPath>
<filtering>true</filtering>
</resource>
</webResources>
</configuration>
</plugin>
<plugin>
<groupId>org.zeroturnaround</groupId>
<artifactId>javarebel-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-rebel-xml</id>
<phase>process-resources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<version>1.0.5</version>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>
org.zeroturnaround
</groupId>
<artifactId>
javarebel-maven-plugin
</artifactId>
<versionRange>
[1.0.5,)
</versionRange>
<goals>
<goal>generate</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
运行效果演示: