前言
本文通过手写Spring,实现Spring MVC基本功能,为学习Spring源码做准备。
Spring介绍
Spring主要由三个阶段:配置阶段、初始化阶段和运行阶段。
- 配置阶段:主要完成application.properties和Annotation配置。
- 初始化阶段:主要加载并解析配置信息,然后初始化IOC容器,完成容器的DI操作,以及完成HandlerMapping的初始化。
- 运行阶段:主要完成Spring容器启动后,完成用户请求的内部调度,并返回响应结果。
开始
1、新建Java Web项目
项目结构如下:

提前安装好Tomcat
New Project -> Java Enterprise -> 勾选Web Application
2、配置阶段
1、创建Servlet类,重写init()、doGet()和doPost()方法。
public class JDispatchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
public void init(ServletConfig config) throws ServletException {
}
}
2、在web.xml添加配置。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>wjmvc</servlet-name>
<servlet-class>com.fang.JDispatchServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>wjmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
在<init-param>中配置了一个初始化加载Spring主配置文件路径,application.properties文件放在/WEB-INF/下,内容如下:
scanPackage=com.fang.demo
3、创建Annotation注解。
@JController注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JController {
String value() default "";
}
@JService注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JService {
String value() default "";
}
@JAutowired注解:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JAutowired {
String value() default "";
}
@JRequestMapping注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JRequestMapping {
String value() default "";
}
@JRequestParam注解:
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JRequestParam {
String value() default "";
}
- ElementType 这个枚举类型的常量提供了一个简单的分类:注释可能出现在Java程序中的语法位置。
- RetentionPolicy这个枚举类型的常量描述保留注释的各种策略,它们与元注释(@Retention)一起指定注释要保留多长时间。
- Documented注解表明这个注释是由 javadoc记录的,在默认情况下也有类似的记录工具。 如果一个类型声明被注释了文档化,它的注释成为公共API的一部分。
4、配置注解
创建com.fang.demo包,这application.properties里的包一致。
IDemoService接口:
public interface IDemoService {
String sayHello(String name);
}
DemoService实现类:
@JService
public class DemoServiceImpl implements IDemoService {
public String sayHello(String name) {
return "Hello, " + name;
}
}
DemoController控制器:
@JController
public class DemoController {
@JAutowired
private IDemoService demoService;
@JRequestMapping("/say")
public String say(@JRequestParam("name") String name) {
return demoService.sayHello(name);
}
}
至此,配置阶段的代码就都已完成。
3、初始化阶段
1、声明成员变量
// 保存所有被扫描到的相关类 com.fang.demo.DemoController,com.fang.demo.DemoServiceImpl
public List<String> clazzNames = new ArrayList<>();
// 保存所有初始化的Bean demoController new DemoController()
public Map<String, Object> iocMap = new HashMap<>();
// 保存所有的url和Method的映射关系 /say Method
public Map<String, Method> handlerMap = new HashMap<>();
// 和web.xml里init-param的值一致
private static final String LOCATION = "contextConfigLocation";
// 保存配置的所有信息
private Properties p = new Properties();
2、重写init方法,实现拿到主配置文件的路径,读取配置文件中的信息,扫描所有相关的类,初始化相关类的实例并保存到IOC容器,从ICO容器取出对应的实例给字段赋值,即依赖注入,最后将url和Method进行关联。
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// 1、加载配置文件
doLoadConfig(config.getInitParameter(LOCATION));
// 2、扫描所有相关的类
doScanner(p.getProperty("scanPackage"));
// 3、初始化所有相关类的实例,并保存到IOC容器中
doInstance();
// 4、依赖注入
doAutowired();
// 5、构造HandlerMapping
initHandlerMapping();
// 6、等待请求,匹配URL,定位方法,反射
System.out.println("Jun Spring is init");
}
doLoadConfig获取主配置文件路径,读取内容保存到Properties对象中:
private void doLoadConfig(String location) {
InputStream is = null;
System.out.println("location = " + location);
is = getServletContext().getResourceAsStream(location);
try {
p.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- Class.getResourceAsStream(String path) : path 不以’/’开头时默认是从此类所在的包下取资源,以’/’开头则是从ClassPath根下获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
- Class.getClassLoader.getResourceAsStream(String path) :默认则是从ClassPath根下获取,path不能以’/’开头,最终是由ClassLoader获取资源。
- ServletContext. getResourceAsStream(String path):默认从WebAPP根目录下取资源,Tomcat下path是否以’/’开头无所谓,当然这和具体的容器实现有关。
doScanner扫描相关类并保存。
private void doScanner(String packageName) {
// com.fang.demo -> com/fang/demo
URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
doScanner(packageName + "." + file.getName());
}
else {
clazzNames.add(packageName + "." + file.getName().replace(".class", "").trim());
}
}
}
正则表达式:
\.表示除换行符\n之外的任何单字符,\\.表示.
doInstance根据类名实例化,并放到IOC容器中。IOC容器的key默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法。
private String lowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
private void doInstance() {
if (clazzNames.size() == 0) {
return;
}
for (String clazzName : clazzNames) {
try {
Class<?> clazz = Class.forName(clazzName);
if (clazz.isAnnotationPresent(JController.class)) {
String beanName = lowerFirstCase(clazz.getSimpleName());
iocMap.put(beanName, clazz.newInstance());
}
else if (clazz.isAnnotationPresent(JService.class)) {
JService service = clazz.getAnnotation(JService.class);
String beanName = service.value();
if (!"".equals(beanName.trim())) {
iocMap.put(beanName, clazz.newInstance());
continue;
}
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> i : interfaces) {
iocMap.put(lowerFirstCase(i.getSimpleName()), clazz.newInstance());
}
}
else {
continue;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
- Class.forName():查找并加载指定的类,返回Class对象。
- Class.getName():获取包名+类名。
- Class.getSimpleName():获取类名。
doAutowired将初始化到IOC容器中的类赋值给有@JAutowired的字段。
private void doAutowired() {
if (iocMap.isEmpty()) {return;}
for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
// 获取所有的属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(JAutowired.class)) {
JAutowired autowired = field.getAnnotation(JAutowired.class);
String beanName = autowired.value();
if ("".equals(beanName)) {
beanName = field.getType().getSimpleName();
beanName = lowerFirstCase(beanName);
}
// 设置可访问
field.setAccessible(true);
try {
// 给对象的属性赋值
field.set(entry.getValue(), iocMap.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
- Class.getFields():获取一个类的public成员变量,包括基类。
- Class.getDeclaredFields():获取一个类的所有成员变量,不包括基类。
- Field.setAccessible成员变量为private,必须进行此操作。
initHandlerMapping将JRequestMapping中配置的信息和Method进行关联,并保存这些关系。
private void initHandlerMapping() {
if (iocMap.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(JController.class)) {continue;}
StringBuilder baseUrl = new StringBuilder("/");
if (clazz.isAnnotationPresent(JRequestMapping.class)) {
JRequestMapping requestMapping = clazz.getAnnotation(JRequestMapping.class);
baseUrl.append(requestMapping.value());
}
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(JRequestMapping.class)) {
JRequestMapping requestMapping = method.getAnnotation(JRequestMapping.class);
baseUrl.append("/" + requestMapping.value());
String url = baseUrl.toString().replaceAll("/+", "/");
handlerMap.put(url, method);
System.out.println("url:" + baseUrl + "," + method);
}
}
}
}
至此,初始化阶段的代码都已完成。
4、运行阶段
在doGet方法调用doPost方法,在doPost方法中再调用doDispach()方法。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req, resp);
}
catch (Exception e) {
resp.getWriter().write("500 Exception: "+ Arrays.toString(e.getStackTrace()));
}
}
doDispatch处理多个//,url请求不存在的返回404,通过url查找到对应的方法进行调用并返回。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
if (handlerMap.isEmpty()) {return;}
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
if (!handlerMap.containsKey(url)) {
resp.getWriter().write("404 Not Founded");
return;
}
Map<String, String[]> params = req.getParameterMap();
Method method = handlerMap.get(url);
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] paramValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> paramType = parameterTypes[i];
if (paramType == HttpServletRequest.class) {
paramValues[i] = req;
}
else if (paramType == HttpServletResponse.class) {
paramValues[i] = resp;
}
else if (paramType == String.class) {
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String value = Arrays.toString(entry.getValue())
.replaceAll("\\[|\\]", "")
.replaceAll("\\s", "");
paramValues[i] = value;
}
}
}
String beanName = lowerFirstCase(method.getDeclaringClass().getSimpleName());
String result = (String) method.invoke(iocMap.get(beanName), paramValues);
resp.getWriter().write(result);
}
正则表达式:
\\[|\\]表示[或]\s表示任何空白字符
至此,一个mini版的Spring就完成了,点击启动,在浏览器输入:http://localhost:8080/say?name=fang,正常返回:Hello, fang,输入其它地址则返回404。
Tomcat配置:Run -> Edit Configuration,Run Configuration点击”+” -> Tomcat Server -> Local,创建一个新的Tomcat,选择本地安装的Tomcat目录。 JavaWeb测试:Run -> Edit Configurations,进入“Run Configurations”窗口,选择之前配置好的Tomcat,点击“Deployment”选项卡,点击“+” -> “Artifact”-> 选择创建的web项目的Artifact…