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) {
// 如果是目录,则递归调用doComponentScan
if (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();
// 只扫描被注解修饰的类BonnieController
if (!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方法
@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();
}
}
/**
* 处理请求:
* 1、解析请求的url获取对应的handler
* 2、解析请求的入参,参数列表,参数类型 以及request response
* 3、反射调用获取结果
* 4、返回对应的结果到前端
* @param req
* @param resp
* @throws Exception
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 获取对应的handler
Handler 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) {
//如果是int
if(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 {
@BonnieAutowired
private 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{
@Override
public 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