手动实现简单的Spring

1. 前言

前两天看到一篇关于手动实现简单的Spring和SpringMVC框架的文章,自己动手跟着做了一遍来加深对Spring框架的理解。

原文地址《自己实现spring核心功能》

项目源码

2. 分析

SpringMVC的关键是请求到servlet后怎么去做映射和处理。首先来看一看dispatherServlet的基本流程
这里先给个简易处理流程
此处输入图片的描述

3. 创建项目

3.1 创建项目

首先创建一个maven项目,代码结构如下:
此处输入图片的描述

  • annotaiton包:存放自定义注解类
  • controller包:存放控制器类,对映SpringMVC项目中的controller包
  • model包:存放实体类
  • service包:存放service类
  • servlet包:存放自定义servlet类

3.2 添加jar包依赖

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>

本项目需要使用到两个jar包
javax.servlet-api包:用来启动核心代码和处理请求
fastjson包:用来处理json类型的数据

4. 项目实现

4.1 创建自定义注解类

创建MyController注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) //作用在类上
public @interface MyController {
String value() default "";
}

创建MyService注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) //作用在类上
public @interface MyService {
String value() default "";
}

创建MyAutoWrited注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) //声明作用在字段上
public @interface MyAutoWrited {
String value() default "";
}

创建MyRequestMapping注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD}) //作用在类和方法上
public @interface MyRequestMapping {
String value() default "";
}

创建MyComponent注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyComponent {
String value() default "";
}

4.2 配置web.xml

需要配置<servlet><servlet-mapping>2个标签

<servlet>中需要

  1. 指定servlet名称
  2. 指定处理请求的前端控制器类
  3. 设置初始化配置文件路径

完整web.xml文件如下:

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>myservletMVC</servlet-name>
<servlet-class>com.myspring.demo.servlet.MyDispatcherServlet</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>myservletMVC</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

4.3 建立测试接口

为了测试依赖注入,所以加了1个controller类、2个接口和2个实现类

HomeController.java

@MyController
@MyRequestMapping("/home")
public class HomeController {
@MyAutoWrited
private IHomeService homeService;

@MyRequestMapping("/sayHi")
public String sayHi() {
return homeService.sayHi();
}

@MyRequestMapping("/getName")
public String getName(Integer id,String no) {
return homeService.getName(id,no);
}
@MyRequestMapping("/getRequestBody")
public String getRequestBody(Integer id, String no, GetUserInfo userInfo) {
return homeService.getRequestBody(id,no,userInfo);
}
}

实体类GetUserInfo.java

public class GetUserInfo {
private String name;
private Integer age;
private BigDecimal growthValue;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public BigDecimal getGrowthValue() {
return growthValue;
}

public void setGrowthValue(BigDecimal growthValue) {
this.growthValue = growthValue;
}
}

IStudentService接口

public interface IStudentService {
String sayHi();
}

IStudentService接口的实现类

@MyService
public class StudentService implements IStudentService {
@Override
public String sayHi(){
return "Hello world!";
}
}

IHomeService接口

public interface IHomeService {
String sayHi();
String getName(Integer id, String no);
String getRequestBody(Integer id, String no, GetUserInfo userInfo);
}

IHomeService接口的实现类

@MyService
public class HomeService implements IHomeService {

@MyAutoWrited
private StudentService studentService;

@Override
public String sayHi() {
return studentService.sayHi();
}

@Override
public String getName(Integer id,String no) {
return "SB0000"+id;
}

@Override
public String getRequestBody(Integer id, String no, GetUserInfo userInfo) {
return "userName="+userInfo.getName()+" no="+no;
}
}

4.4 初始化MyDispatcherServlet类

MyDispatcherServlet类专门处理servlet请求,匹配到对应的方法执行后返回

这里我们创建一个叫MyDispatcherServlet的类,它继承HttpServlet类,并且重写HttpServlet的init(),doGet(),doPost()这3个方法

public class MyDispatcherServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}

@Override
public void init(ServletConfig config) {

}
}

4.4.1 初始化分析

初始化工作是框架完成依赖注入的关键,我们在MyDispatcherServlet类的init()方法中,实现如下业务逻辑,就能将spring功能给初始化了,就可以使用依赖注入了

public void init(ServletConfig config) {
//加载配置
String contextConfigLocation = config.getInitParameter("contextConfigLocation");
loadConfig(contextConfigLocation);

//获取要扫描的包地址
String dirpath = properties.getProperty("scanner.package");

//扫描要加载的类
doScanner(dirpath);

//实例化要加载的类
doInstance();

//加载依赖注入,给属性赋值
doAutoWrited();

//加载映射地址
doRequestMapping();
}

4.4.2 加载配置

String contextConfigLocation = config.getInitParameter("contextConfigLocation");
loadConfig(contextConfigLocation);

这里会获取到web.xml中init-param节点中的值

<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>

具体指向的是resources文件下的application.properties配置文件,里面只有一行配置

scanner.package=com.myspring.demo

这是指定了需要扫描的包路径

loadConfig()方法是将application.properties文件中的包路径信息加载到properties对象中

/**
* 加载application.properties文件
* @param contextConfigLocation
*/
private void loadConfig(String contextConfigLocation) {

InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
assert is != null;
properties.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

4.4.3 扫描要加载的类

定义一个成员变量beanNames列表用来存放扫描到的需要加载类的路径

private List<String> beanNames = new ArrayList<String>();

从properties对象中获取刚刚加载的要扫描的包路径信息

//获取要扫描的包地址
String dirpath = properties.getProperty("scanner.package");

此处输入图片的描述
就是要扫描图中红色方框中的包

定义doScanner方法,用递归方式扫描要加载的类

/**
* 通过递归扫描../com/myspring/demo包下的所有文件
* @param dirpath
*/
private void doScanner(String dirpath) {
URL url = this.getClass().getClassLoader().getResource("/" + dirpath.replaceAll("\\.", "/"));
assert url != null;
File dir = new File(url.getFile());
File[] files = dir.listFiles();//获取文件列表
assert files != null;
for (File file : files) {
if (file.isDirectory()) {//判断file是否是文件夹,若是文件夹则进行递归扫描
doScanner(dirpath + "." + file.getName());
continue;
}

//取文件名
String beanName = dirpath + "." + file.getName().replaceAll(".class", "");
beanNames.add(beanName); //将扫描到的文件名添加到beanNames列表中
}
}

此时可打断点调试看是否能正确扫描到类信息,如图:
此处输入图片的描述

4.4.4 实例化要加载的类

我们已经得到了这些定义好的类的名称列表,现在我们需要一个个实例化,并且保存在ioc容器当中。
先定义个装载类的容器,使用HashMap就能做到

private Map<String, Object> ioc = new HashMap<>();

创建doInstance()方法进行实例化

/**
* 实例化需要实例的类
*/
private void doInstance() {
if (beanNames.isEmpty()) {
return;
}
for (String beanName : beanNames) {
try {
Class cls = Class.forName(beanName);
if (cls.isAnnotationPresent(MyController.class)) { //判断类是否有MyController注解修饰
//使用反射实例化对象
Object instance = cls.newInstance();
//默认类名首字母小写
beanName = firstLowerCase(cls.getSimpleName()); //获取类的类名(不含有路径)
//写入ioc容器
ioc.put(beanName, instance);


} else if (cls.isAnnotationPresent(MyService.class)) { //判断类是否有MyService注解修饰
Object instance = cls.newInstance();
MyService MyService = (MyService) cls.getAnnotation(MyService.class);

String alisName = MyService.value();
if (alisName==null || alisName.trim().length() == 0) { //判断是否有注解值
beanName = cls.getSimpleName(); //若没有注解值把类名作为key放在ioc容器中
} else {
beanName = alisName; //若有注解值把注解值作为key放在ioc容器中
}
beanName = firstLowerCase(beanName);
ioc.put(beanName, instance);
//如果是接口,自动注入它的实现类
Class<?>[] interfaces = cls.getInterfaces();
for (Class<?> c :
interfaces) {
ioc.put(firstLowerCase(c.getSimpleName()), instance);
}
} else {
continue;
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}

其中firstLowerCase(String str)方法将类名的首字母小写后存入ioc

/**
* 将str首字母改为小写
* @param str
* @return
*/
private String firstLowerCase(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}

只要提供类的完全限定名,通过Class.forName静态方法,就能将类信息加载到内存中并且返回Class 对象,通过反射来实例化,见第10行代码。

通过循环beanNames集合,来实例化需要加载每个类,并将实例化后的对象装入HashMap中(目前需要实例化的类只有包含MyControllerMyService注解的类)

实例化完成后,ioc容器中的数据如下:
此处输入图片的描述

图片中可以看出,hashMap的key都是小写,value已经是对象了。红框表示类中的字段属性,此时可以看到,虽然类已经被实例化了,但是属性还是null,需要进行接下来的依赖注入。

4.4.5 进行依赖注入,给属性赋值

定义一个无参的方法doAutoWrite()进行依赖注入

private void doAutoWrited() {

for (Map.Entry<String, Object> obj : ioc.entrySet()) {//entrySet()方法返回map中各个键值对映射关系的集合
try {
for (Field field : obj.getValue().getClass().getDeclaredFields()) {//获取对象所有的属性
if (!field.isAnnotationPresent(MyAutoWrited.class)) {//判断属性是否有MyAutoWrited注解
continue;
}
MyAutoWrited autoWrited = field.getAnnotation(MyAutoWrited.class);
String beanName = autoWrited.value();
if ("".equals(beanName)) {
beanName = field.getType().getSimpleName();//获取类名
}

field.setAccessible(true);

field.set(obj.getValue(), ioc.get(firstLowerCase(beanName)));//为属性值注入实体
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

这个方法是通过循环遍历ioc里面的实体,反射找出字段,看看是否有需要注入的标记MyAutoWrited,如果加了标记,就反射给字段赋值,类型从ioc容器中获取。

4.4.6 加载映射地址

定义一个HashMap对象urlMapping用来存放url和方法之间的映射

private Map<String, Method> urlMapping = new HashMap<>();

映射地址的作用是根据请求的url匹配method方法

新建doRequestMapping()方法加载映射地址;

private void doRequestMapping() {

if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> obj : ioc.entrySet()) {//遍历ioc容器里的实体,找到含有MyController注解的对象
if (!obj.getValue().getClass().isAnnotationPresent(MyController.class)) {
continue;
}
Method[] methods = obj.getValue().getClass().getMethods();//获取对象中的所有方法
for (Method method : methods) {//遍历对象中的方法,把有MyRequestMapping注解的方法
if (!method.isAnnotationPresent(MyRequestMapping.class)) {
continue;
}
String baseUrl = "";
if (obj.getValue().getClass().isAnnotationPresent(MyRequestMapping.class)) {
baseUrl = obj.getValue().getClass().getAnnotation(MyRequestMapping.class).value();
}
MyRequestMapping MyRequestMapping = method.getAnnotation(MyRequestMapping.class);
if ("".equals(MyRequestMapping.value())) {
continue;
}
String url = (baseUrl + "/" + MyRequestMapping.value()).replaceAll("/+", "/");
urlMapping.put(url, method);//把方法的url作为key,放入urlMapping容器中
System.out.println(url);
}
}
}

这里其实就是根据对象反射获取到MyRequestMapping上面的value值
@MyRequestMapping("/sayHi")取到的就是/sayHi

4.5 请求转发和参数解析

在浏览器发起请求localhost:8080/home/sayHi,请求会到达MyDispatherServlet类,由于是GET请求
最终会走到doDispatcherServlet方法里面处理请求

@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 {
doDispatcherServlet(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}

定义doDispatcherServlet方法处理请求

private void doDispatcherServlet(HttpServletRequest req, HttpServletResponse resp) throws IOException, InvocationTargetException, IllegalAccessException {
String url = req.getRequestURI();//获取请求的url
url = url.replace(req.getContextPath(), "").replaceAll("/+", "/");
if (!urlMapping.containsKey(url)) {//判断urlMapping中是否有对应的url
resp.getWriter().write("404! url is not found!");
return;
}

Method method = urlMapping.get(url);//获取对应的处理方法
String className = method.getDeclaringClass().getSimpleName();
className = firstLowerCase(className);
if (!ioc.containsKey(className)) {
resp.getWriter().write("500! claas not defind !");
return;
}
Object[] args ;//参数列表
if ("POST".equals(req.getMethod()) && req.getContentType().contains("json")) {//判断请求的参数格式
String str = getJson(req);
args = getRequestParam(str, method);//处理json格式的参数
} else {
args = getRequestParam(req.getParameterMap(), method);//处理form格式参数
}
//调用目标方法
Object res = method.invoke(ioc.get(className), args);

resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write(res.toString());
}

post请求参数处理

处理json格式参数

private Object[] getRequestParam(String json, Method method) {
if (null == json || json.isEmpty()) {
return null;
}
Parameter[] parameters = method.getParameters();
Object[] requestParam = new Object[parameters.length];
JSONObject jsonObject = JSONObject.parseObject(json);
int i = 0;
for (Parameter p : parameters) {
Object val = jsonObject.getObject(p.getName(), p.getType());
requestParam[i] = val;
i++;
}
return requestParam;
}

处理form参数

private Object[] getRequestParam(Map<String, String[]> map, Method method) {
if (null == map || map.size() == 0) {
return null;
}
Parameter[] parameters = method.getParameters();
int i = 0;
Object[] requestParam = new Object[parameters.length];
for (Parameter p : parameters) {
if (!map.containsKey(p.getName())) {
requestParam[i] = null;
i++;
continue;
}
try {
Class typeClass = p.getType();
String[] val = map.get(p.getName());
if (null == val) {
requestParam[i] = null;
i++;
continue;
}
Constructor con = null;
try {
con = typeClass.getConstructor(val[0].getClass());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
Object obj = null;
try {
assert con != null;
obj = con.newInstance(val[0]);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
requestParam[i] = obj;
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
i++;
}
return requestParam;
}

5. 结果测试

用浏览器访问/home/sayHi发现可以正常访问
此处输入图片的描述

用postman测试form请求可以获取数据
此处输入图片的描述

用postman测试json请求也可以获取数据
此处输入图片的描述

-------------本文结束感谢您的阅读-------------