01-spring-手写spring-demo实现基础的功能
1 目标
自定义BonnieController BonnieRequestMapping、BonnieService、BonnieAutowired等注解实现一个简单的springmvc的功能。从浏览器访问url,然后请求到对应的接口,以及service等。
2 流程说明
3、代码获取途径
以下是下载地址以及分支: 如果需要自己开发,可以基于20250808-base分支,这个分支仅搭建好了基础的框架
地址 | 分支 |
https://gitee.com/huyanqiu6666/customize-spring.git | 20250808-springV1.0 |
4 重点代码说明
4.1 入口web.xml
在web.xml中配置了servlet接收所有请求,调用它的doGet或者doPost方法;服务启动的时候调用DispatcherServlet.init方法,在这里可以做初始化阶段的所有操作。
<?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>Bonnie Web Application</display-name><servlet><servlet-name>bonnieMvc</servlet-name><servlet-class>com.bonnie.servlet.DispatcherServlet</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>bonnieMvc</servlet-name><url-pattern>/*</url-pattern></servlet-mapping>
</web-app>
4.2 获取servlet配置init-param信息
// 1、加载配置文件 String contextConfigLocation = config.getInitParameter("contextConfigLocation");
/*** 加载配置文件 - application.properties* @param contextConfigLocation*/private void doLoadConfig(String contextConfigLocation) {// 加载配置文件流InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);try {contextPropertiesConfig.load(is);} catch (IOException e) {e.printStackTrace();}}
4.3 扫描相关类
/*** 所有类的全路径集合*/ private List<String> classNameList = new ArrayList<>();
String scanPackage = contextPropertiesConfig.get("componentScan").toString();
private void doComponentScan(String scanPackage) {try {/*** scanPackage = com.bonnie, 转化为com/bonnie*/String scanPackagePath = scanPackage.replaceAll("\\.", "/");URL url = this.getClass().getClassLoader().getResource("/" + scanPackagePath);File[] fileArray = new File(url.getFile()).listFiles();for (File file : fileArray) {// 如果是目录,则递归调用doComponentScanif (file.isDirectory()) {String newPackage = scanPackage + "." + file.getName();doComponentScan(newPackage);} else {if (!file.getName().endsWith(".class")) {continue;}// 类的全路径String classFullName = scanPackage + "." + file.getName().replace(".class", "");System.out.println("扫描的classFullName:" + classFullName);classNameList.add(classFullName);}}} catch (Exception e) {e.printStackTrace();}}
4.4 将类放入到IOC容器中
/*** 简化IOC容器*/ private Map<String,Object> iocMap = new ConcurrentHashMap<>();
/*** 初始化扫描到的所有类,放到到IOC容器中*/private void doInstance() {if (CollectionUtils.isEmpty(classNameList)) {return;}try {for (String className : classNameList) {Class<?> clazz = Class.forName(className);/*** 只有被自定义注解的类才可以被加载到IOC容器中*/if (clazz.isAnnotationPresent(BonnieController.class)) {IocLoadUtils.loadController(clazz, iocMap);System.out.println("ioc 扫描到: " + clazz.getName());} else if (clazz.isAnnotationPresent(BonnieService.class)) {IocLoadUtils.loadService(clazz, iocMap);System.out.println("ioc 扫描到: " + clazz.getName());} else {continue;}}} catch (Exception e) {e.printStackTrace();}}
package com.bonnie.utils;import com.bonnie.annotations.BonnieService;
import org.apache.commons.lang3.StringUtils;import java.util.Map;public class IocLoadUtils {public static void loadController(Class<?> clazz, Map<String, Object> iocMap) throws InstantiationException, IllegalAccessException {Object object = clazz.newInstance();String beanName = toLowerFirstCase(clazz.getSimpleName());iocMap.put(beanName, object);}public static void loadService(Class<?> clazz, Map<String, Object> iocMap) throws InstantiationException, IllegalAccessException {BonnieService bonnieService = clazz.getAnnotation(BonnieService.class);String beanName = bonnieService.value().trim();if (StringUtils.isEmpty(beanName)) {beanName = toLowerFirstCase(clazz.getSimpleName());}Object object = clazz.newInstance();iocMap.put(beanName, object);// 根据类型自动赋值 接口for (Class<?> classInterface : clazz.getInterfaces()) {if (iocMap.containsKey(classInterface.getName())) {throw new RuntimeException(classInterface.getName() + " 已经在IOCMap中");}iocMap.put(classInterface.getName(), object);}}private static String toLowerFirstCase(String simpleName) {char [] chars = simpleName.toCharArray();//之所以加,是因为大小写字母的ASCII码相差32,// 而且大写字母的ASCII码要小于小写字母的ASCII码//在Java中,对char做算学运算,实际上就是对ASCII码做算学运算chars[0] += 32;return String.valueOf(chars);}}
4.5 依赖注入
/*** 依赖注入*/private void doBonnieAutowired() {if (iocMap.isEmpty()) {return;}Set<Map.Entry<String, Object>> entries = iocMap.entrySet();for (Map.Entry<String, Object> entry : entries) {Object beanInstance = entry.getValue();// 获取类中的所有字段(成员变量)Field[] declaredFields = beanInstance.getClass().getDeclaredFields();for (Field field : declaredFields) {if (!field.isAnnotationPresent(BonnieAutowired.class)) {continue;}// 如果指定bean名称,则使用指定的,否则使用类的名称String autowiredBeanName = field.getAnnotation(BonnieAutowired.class).value().trim();if (StringUtils.isEmpty(autowiredBeanName)) {autowiredBeanName = field.getType().getName();}if (!iocMap.containsKey(autowiredBeanName)) {throw new RuntimeException("Ioc容器中不存在类,无法依赖注入:" + autowiredBeanName);}// 开启暴力访问field.setAccessible(true);try {// 用反射机制,动态给字段赋值field.set(beanInstance, iocMap.get(autowiredBeanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}
4.6 初始化handlerMapping
/*** requestHandler集合*/ private List<Handler> requestHandlerMapping = new ArrayList<Handler>();
/*** 初始化HandlerMapping*/private void initHandlerMapping() {if (iocMap.isEmpty()) {return;}Set<Map.Entry<String, Object>> entries = iocMap.entrySet();for (Map.Entry<String, Object> entry : entries) {Class<?> beanClazz = entry.getValue().getClass();// 只扫描被注解修饰的类BonnieControllerif (!beanClazz.isAnnotationPresent(BonnieController.class)) {continue;}String baseUrl = StringUtils.EMPTY;if (beanClazz.isAnnotationPresent(BonnieRequestMapping.class)) {BonnieRequestMapping requestMapping = beanClazz.getAnnotation(BonnieRequestMapping.class);baseUrl = requestMapping.value();}// 获取所有的public方法Method[] methods = beanClazz.getMethods();for (Method method : methods) {BonnieRequestMapping methodRequestMapping = method.getAnnotation(BonnieRequestMapping.class);if (Objects.isNull(methodRequestMapping)) {// 先不抛出异常continue;}String regex = ("/" + baseUrl + "/" + methodRequestMapping.value()).replaceAll("/+","/");Pattern pattern = Pattern.compile(regex);System.out.println("扫描到的请求url: " + pattern);Handler handler = new Handler();handler.setPattern(pattern);handler.setController(entry.getValue());handler.setMethod(method);handler.setParamTypes(method.getParameterTypes());handler.putParamIndexMapping(method);requestHandlerMapping.add(handler);}}}
4.7 运行阶段
doGet方法使用doPost方法
@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}
@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {try {// 真实处理业务请求doDispatch(req, resp);} catch (Exception e) {e.printStackTrace();}}
/*** 处理请求:* 1、解析请求的url获取对应的handler* 2、解析请求的入参,参数列表,参数类型 以及request response* 3、反射调用获取结果* 4、返回对应的结果到前端* @param req* @param resp* @throws Exception*/private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {// 获取对应的handlerHandler handler = RequestHandlerMappingUtils.getHandler(req, requestHandlerMapping);if (Objects.isNull(handler)) {resp.getWriter().write("404 Not Found!!!");return;}// 方法的形参列表Class<?>[] paramTypes = handler.getParamTypes();// 定义参数列表Object [] paramValues = new Object[paramTypes.length];Map<String,String[]> parameterMap = req.getParameterMap();for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]","").replaceAll("\\s",",");Map<String, Integer> paramIndexMapping = handler.getParamIndexMapping();if (!paramIndexMapping.containsKey(param.getKey())) {continue;}int index = handler.getParamIndexMapping().get(param.getKey());paramValues[index] = convert(paramTypes[index],value);}if(handler.getParamIndexMapping().containsKey(HttpServletRequest.class.getName())) {int reqIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getName());paramValues[reqIndex] = req;}if(handler.getParamIndexMapping().containsKey(HttpServletResponse.class.getName())) {int respIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getName());paramValues[respIndex] = resp;}// 反射调用,获取结果Object returnValue = handler.getMethod().invoke(handler.getController(),paramValues);if (returnValue == null || returnValue instanceof Void){return;}resp.getWriter().write(returnValue.toString());}private Object convert(Class<?> paramType, String value) {//如果是intif(Integer.class == paramType){return Integer.valueOf(value);}else if(Double.class == paramType){return Double.valueOf(value);}else if (Long.class == paramType) {return Long.parseLong(value);}//如果还有double或者其他类型,继续加if// 可以使用策略模式了return value;}
package com.bonnie.utils;import com.bonnie.entity.Handler;import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.regex.Matcher;public class RequestHandlerMappingUtils {/*** 获取请求对应的handler** @param req* @param requestHandlerMapping* @return*/public static Handler getHandler(HttpServletRequest req, List<Handler> requestHandlerMapping) {// 绝对路径String requestURI = req.getRequestURI();String contextPath = req.getContextPath();// 相对路径String url = requestURI.replaceAll(contextPath, "").replaceAll("/+", "/");for (Handler handler : requestHandlerMapping) {Matcher matcher = handler.getPattern().matcher(url);if (!matcher.matches()) {continue;}return handler;}return null;}}
5 运行结果
5.1 测试类
package com.bonnie.controller;import com.bonnie.annotations.BonnieAutowired;
import com.bonnie.annotations.BonnieController;
import com.bonnie.annotations.BonnieRequestMapping;
import com.bonnie.annotations.BonnieRequestParam;
import com.bonnie.service.UserService;@BonnieController
@BonnieRequestMapping("/user")
public class UserController {@BonnieAutowiredprivate UserService userService;@BonnieRequestMapping("/user")public String findById(@BonnieRequestParam("id") Long id) {return userService.findById(id);}}
package com.bonnie.service;import com.bonnie.annotations.BonnieService;import java.text.SimpleDateFormat;
import java.util.Date;@BonnieService
public class UserServiceImpl implements UserService{@Overridepublic String findById(Long id) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String now = simpleDateFormat.format(new Date());return "userId:" + id + " Date: "+ now;}}
5.2 启动
启动部分日志
扫描的classFullName:com.bonnie.annotations.BonnieAutowired
扫描的classFullName:com.bonnie.annotations.BonnieController
扫描的classFullName:com.bonnie.annotations.BonnieRequestMapping
扫描的classFullName:com.bonnie.annotations.BonnieRequestParam
扫描的classFullName:com.bonnie.annotations.BonnieService
扫描的classFullName:com.bonnie.controller.UserController
扫描的classFullName:com.bonnie.entity.Handler
扫描的classFullName:com.bonnie.service.UserService
扫描的classFullName:com.bonnie.service.UserServiceImpl
扫描的classFullName:com.bonnie.servlet.DispatcherServlet
扫描的classFullName:com.bonnie.servlet.MyServlet
扫描的classFullName:com.bonnie.utils.IocLoadUtils
扫描的classFullName:com.bonnie.utils.RequestHandlerMappingUtils
ioc 扫描到: com.bonnie.controller.UserController
ioc 扫描到: com.bonnie.service.UserServiceImpl
扫描到的请求url: /user/user
Bonnie Spring framework is init.
11-Aug-2025 15:01:38.596 信息 [main] org.apache.catalina.startup.HostConfig.deployDescriptor 部署描述符[C:\Users\L14\.SmartTomcat\customize-spring\customize-spring\conf\Catalina\localhost\customize-spring.xml]的部署已在[2,704]ms内完成
11-Aug-2025 15:01:38.618 信息 [main] org.apache.coyote.AbstractProtocol.start 开始协议处理句柄["http-nio-8080"]
11-Aug-2025 15:01:38.873 信息 [main] org.apache.catalina.startup.Catalina.start [3255]毫秒后服务器启动
http://localhost:8080/customize-spring