предисловие
Заголовок «Реализация простой Java MVC Framework с нуля», и в результате я так много написал, прежде чем пришел к реализации MVC... Можно только сказать, что прелюдия действительно слишком длинная. Однако все эти прелюдии необходимы. Если вы просто реализуете функцию MVC, это будет бессмысленно. Вам нужен контейнер Bean, IOC, AOP и MVC, чтобы быть похожим на «каркас».
готовность к реализации
Чтобы реализовать функцию mvc, мы должны сначала добавить некоторые зависимости в pom.xml.
<properties>
...
<tomcat.version>8.5.31</tomcat.version>
<jstl.version>1.2</jstl.version>
<fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
...
<!-- tomcat embed -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
<scope>runtime</scope>
</dependency>
<!-- FastJson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
-
tomcat-embed-jasper
Эта зависимость вводит встроенный tomcat,spring-bootПо умолчанию этот встроенный пакет tomcat используется для непосредственного запуска службы. В дополнение к добавлению встроенного кота, этот пакет также представляетjava.servlet-api
иjsp-api
Эти два пакета, если вы не хотите использовать этот встроенный кот, вы можете удалить ихtomcat-embed-jasper
Затем импортируйте эти два пакета. -
jstl
Например, для анализа выражений jsp напишите следующее на странице jspc:forEach
Заявления нуждаются в этом пакете.<c:forEach items="${list}" var="user"> <tr> <td>${user.id}</td> <td>${user.name}</td> </tr> </c:forEach>
-
fastjson
Это пакет синтаксического анализа json, разработанный Али для преобразования классов сущностей в json. Подобные пакеты такжеGson
иJackson
Подождите, здесь нет конкретного сравнения, вы можете выбрать то, что вам нравится.
Реализовать MVC
Принцип реализации MVC
Прежде всего, нам нужно понять принцип реализации MVC.spring-bootПри написании проекта мы обычно реализуем ссылку, написав серию контроллеров, что является «современным» способом написания. но преждеspringmvcЧетноеstruts2Когда такой фреймворк mvc не был популярен, его писали, написавServlet
реализовать.
Каждому запросу будет соответствоватьServlet
, а затем также настроить это в web.xmlServlet
, а затем прием и обработка запросов распределяются по большому количествуServlet
, код очень смешанный.
Чтобы люди могли писать более сосредоточенно на бизнес-коде и сокращать обработку запросов,springmvcчерез центральныйServlet
, обрабатывать эти запросы, а затем пересылать их соответствующему Контроллеру, чтобы остался только одинServlet
Единая обработка запросов. Следующий абзац взят из официальной документации springdocs.spring.IO/весна/документы…
Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central
Servlet
, theDispatcherServlet
, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.The
DispatcherServlet
, as anyServlet
, needs to be declared and mapped according to the Servlet specification using Java configuration or inweb.xml
. In turn theDispatcherServlet
uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.
Это примерно означает:springmvcРабота контроллера управления реализована через центральный сервлет (DispatcherServlet). этоServlet
Для настройки или настройки в web.xml через java используется для поиска маппинга запроса (то есть для поиска соответствующего контроллера), парсинга вида (то есть результата выполнения контроллера), обработки исключений (то есть то есть унифицированная обработка исключений в процессе выполнения) и т.д. Подождите
Итак, эффект реализации MVC следующий:
- через центральный sevlet, такой как
DispatcherServlet
получать все запросы - Найдите соответствующий контроллер по запросу
- Выполните контроллер, чтобы получить результат
- Разобрать результат контроллера и перейти к соответствующему представлению
- Если есть исключение, обработайте исключение единообразно
В соответствии с приведенными выше шагами мы начинаем с шагов 2, 3, 4, 5 и, наконец, реализуем 1 для завершения mvc.
Создание аннотаций
Чтобы упростить реализацию, сначала создайте три аннотации и перечисление в пакете com.zbw.mvc.annotation:RequestMapping
,RequestParam
,ResponseBody
,RequestMethod
.
package com.zbw.mvc.annotation;
import ...
/**
* http请求路径
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/**
* 请求路径
*/
String value() default "";
/**
* 请求方法
*/
RequestMethod method() default RequestMethod.GET;
}
package com.zbw.mvc.annotation;
/**
* http请求类型
*/
public enum RequestMethod {
GET, POST
}
package com.zbw.mvc.annotation;
import ...
/**
* 请求的方法参数名
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
/**
* 方法参数别名
*/
String value() default "";
/**
* 是否必传
*/
boolean required() default true;
}
package com.zbw.mvc.annotation;
import ...
/**
* 用于标记返回json
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}
Функции этих категорий объяснять не будем, все ониspringmvcСамая обычная аннотация.
Создать модель и представление
Чтобы легко передавать параметры во внешний интерфейс, создайте компонент-инструмент, который эквивалентенspringупрощенная версияModelAndView
. Этот класс создан в пакете com.zbw.mvc.bean.
package com.zbw.mvc.bean;
import ...
/**
* ModelAndView
*/
public class ModelAndView {
/**
* 页面路径
*/
private String view;
/**
* 页面data数据
*/
private Map<String, Object> model = new HashMap<>();
public ModelAndView setView(String view) {
this.view = view;
return this;
}
public String getView() {
return view;
}
public ModelAndView addObject(String attributeName, Object attributeValue) {
model.put(attributeName, attributeValue);
return this;
}
public ModelAndView addAllObjects(Map<String, ?> modelMap) {
model.putAll(modelMap);
return this;
}
public Map<String, Object> getModel() {
return model;
}
}
Реализовать диспетчер контроллера
Диспетчер контроллера аналогичен контейнеру бинов, за исключением того, что в последнем хранятся бины, а в первом — контроллеры, и тогда соответствующий контроллер можно просто получить согласно некоторым условиям.
Сначала создайте его в пакете com.zbw.mvc.ControllerInfo
Класс используется для хранения некоторой информации о контроллере.
package com.zbw.mvc;
import ...
/**
* ControllerInfo 存储Controller相关信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
/**
* controller类
*/
private Class<?> controllerClass;
/**
* 执行的方法
*/
private Method invokeMethod;
/**
* 方法参数别名对应参数类型
*/
private Map<String, Class<?>> methodParameter;
}
а затем создать еще одинPathInfo
Класс, используемый для хранения пути запроса и типа метода запроса.
package com.zbw.mvc;
import ...
/**
* PathInfo 存储http相关信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
/**
* http请求方法
*/
private String httpMethod;
/**
* http请求路径
*/
private String httpPath;
}
Затем создайте класс диспетчера контроллера.ControllerHandler
package com.zbw.mvc;
import ...
/**
* Controller 分发器
*/
@Slf4j
public class ControllerHandler {
private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();
private BeanContainer beanContainer;
public ControllerHandler() {
beanContainer = BeanContainer.getInstance();
Set<Class<?>> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
for (Class<?> clz : classSet) {
putPathController(clz);
}
}
/**
* 获取ControllerInfo
*/
public ControllerInfo getController(String requestMethod, String requestPath) {
PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
return pathControllerMap.get(pathInfo);
}
/**
* 添加信息到requestControllerMap中
*/
private void putPathController(Class<?> clz) {
RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
String basePath = controllerRequest.value();
Method[] controllerMethods = clz.getDeclaredMethods();
// 1. 遍历Controller中的方法
for (Method method : controllerMethods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
// 2. 获取这个方法的参数名字和参数类型
Map<String, Class<?>> params = new HashMap<>();
for (Parameter methodParam : method.getParameters()) {
RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
if (null == requestParam) {
throw new RuntimeException("必须有RequestParam指定的参数名");
}
params.put(requestParam.value(), methodParam.getType());
}
// 3. 获取这个方法上的RequestMapping注解
RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
String methodPath = methodRequest.value();
RequestMethod requestMethod = methodRequest.method();
PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
if (pathControllerMap.containsKey(pathInfo)) {
log.error("url:{} 重复注册", pathInfo.getHttpPath());
throw new RuntimeException("url重复注册");
}
// 4. 生成ControllerInfo并存入Map中
ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
this.pathControllerMap.put(pathInfo, controllerInfo);
log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}",
pathInfo.getHttpMethod(), pathInfo.getHttpPath(),
controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
}
}
}
}
Самая сложная часть этого класса — та, которая вызывается в конструкторе.putPathController()
метод, этот метод также является основным методом этого класса, который понимает, что информация в классе контроллера хранится вpathControllerMap
функции в переменных. Кратко объясните некоторые функциональные процессы этого класса:
- Получить контейнер Bean в конструкторе
BeanContainer
одноэлементный экземпляр - получить и повторить
BeanContainer
Хранится вRequestMapping
Классы, отмеченные аннотациями - Пройдитесь по методам этого класса, чтобы найти
RequestMapping
Метод разметки аннотации - Получите имя параметра и тип параметра этого метода, сгенерируйте
ControllerInfo
- в соответствии с
RequestMapping
внутреннийvalue()
иmethod()
генерироватьPathInfo
- будет генерировать
PathInfo
иControllerInfo
сохранить в переменнуюpathControllerMap
середина - другие занятия по телефону
getController()
способ получить соответствующий контроллер
Вышеупомянутый процесс этого класса, есть замечание:
На шаге 4 необходимо указать, что все имена параметров этого методаRequestParam
Аннотация Аннотация, это потому, что в java, хотя у нас есть имена параметров, когда мы пишем код, напримерString name
Эта форма, но поле «имя» будет стерто после компиляции в файл класса, поэтому ему необходимо передатьRequestParam
чтобы сохранить имя.
Но всеspringmvcНет необходимости помечать каждый метод аннотациями, потому что Spring использует *asm
* , этот инструмент может получить имена параметров и сохранить их перед компиляцией. Другой метод заключается в поддержке сохранения имен параметров после java8, но параметры компилятора должны быть изменены для поддержки. Эти два метода более сложны в реализации или имеют ограничения, поэтому здесь они реализовываться не будут, вы можете найти информацию и реализовать их самостоятельно.
Реализовать исполнитель результатов
Затем реализуйте исполнитель результатов, который только что реализует шаги 3, 4 и 5 в процессе mvc.
Создайте класс в пакете com.zbw.mvc.ResultRender
package com.zbw.mvc;
import ...
/**
* 结果执行器
*/
@Slf4j
public class ResultRender {
private BeanContainer beanContainer;
public ResultRender() {
beanContainer = BeanContainer.getInstance();
}
/**
* 执行Controller的方法
*/
public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
// 1. 获取HttpServletRequest所有参数
Map<String, String> requestParam = getRequestParams(req);
// 2. 实例化调用方法要传入的参数值
List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);
Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
Method invokeMethod = controllerInfo.getInvokeMethod();
invokeMethod.setAccessible(true);
Object result;
// 3. 通过反射调用方法
try {
if (methodParams.size() == 0) {
result = invokeMethod.invoke(controller);
} else {
result = invokeMethod.invoke(controller, methodParams.toArray());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
// 4.解析方法的返回值,选择返回页面或者json
resultResolver(controllerInfo, result, req, resp);
}
/**
* 获取http中的参数
*/
private Map<String, String> getRequestParams(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
//GET和POST方法是这样获取请求参数的
request.getParameterMap().forEach((paramName, paramsValues) -> {
if (ValidateUtil.isNotEmpty(paramsValues)) {
paramMap.put(paramName, paramsValues[0]);
}
});
// TODO: Body、Path、Header等方式的请求参数获取
return paramMap;
}
/**
* 实例化方法参数
*/
private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
return methodParams.keySet().stream().map(paramName -> {
Class<?> type = methodParams.get(paramName);
String requestValue = requestParams.get(paramName);
Object value;
if (null == requestValue) {
value = CastUtil.primitiveNull(type);
} else {
value = CastUtil.convert(type, requestValue);
// TODO: 实现非原生类的参数实例化
}
return value;
}).collect(Collectors.toList());
}
/**
* Controller方法执行后返回值解析
*/
private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
if (null == result) {
return;
}
boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
if (isJson) {
// 设置响应头
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
// 向响应中写入数据
try (PrintWriter writer = resp.getWriter()) {
writer.write(JSON.toJSONString(result));
writer.flush();
} catch (IOException e) {
log.error("转发请求失败", e);
// TODO: 异常统一处理,400等...
}
} else {
String path;
if (result instanceof ModelAndView) {
ModelAndView mv = (ModelAndView) result;
path = mv.getView();
Map<String, Object> model = mv.getModel();
if (ValidateUtil.isNotEmpty(model)) {
for (Map.Entry<String, Object> entry : model.entrySet()) {
req.setAttribute(entry.getKey(), entry.getValue());
}
}
} else if (result instanceof String) {
path = (String) result;
} else {
throw new RuntimeException("返回类型不合法");
}
try {
req.getRequestDispatcher("/templates/" + path).forward(req, resp);
} catch (Exception e) {
log.error("转发请求失败", e);
// TODO: 异常统一处理,400等...
}
}
}
}
позвонив в классinvokeController()
Отражение метода вызывает метод в контроллере и анализирует соответствующую страницу в соответствии с результатом. Основной процесс:
- перечислить
getRequestParams()
Получить параметры в HttpServletRequest - перечислить
instantiateMethodArgs()
Значения параметров, которые необходимо передать для создания экземпляра метода вызова - Вызвать целевой метод целевого контроллера через отражение
- перечислить
resultResolver()
Разберите возвращаемое значение метода, выберите возвращаемую страницу или json
Благодаря этим шагам основные шаги MVC сжаты, однако из-за нехватки места функции почти каждого шага упрощены, например
- Шаг 1 При получении параметров в HttpServletRequest получаются только параметры, переданные get или post.На самом деле, есть еще такие параметры запроса, как Body, Path, Header и т. д., которые не реализованы.
- Шаг 2 Значение создания экземпляра вызывающего метода реализует только собственные параметры java, а создание экземпляра пользовательского класса не реализовано.
- Шаг 4. Не реализована унифицированная обработка исключений.
Несмотря на недостатки, процесс MVC завершен. Следующим шагом является сборка этих функций.
Реализовать DispatcherServlet
Наконец-то дошло до того, что было сказано в началеDispatcherServlet
, этот класс наследуется отHttpServlet
, через который проходят все запросы.
Создано под com.zbw.mvcDispatcherServlet
package com.zbw.mvc;
import ...
/**
* DispatcherServlet 所有http请求都由此Servlet转发
*/
@Slf4j
public class DispatcherServlet extends HttpServlet {
private ControllerHandler controllerHandler = new ControllerHandler();
private ResultRender resultRender = new ResultRender();
/**
* 执行请求
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置请求编码方式
req.setCharacterEncoding("UTF-8");
//获取请求方法和请求路径
String requestMethod = req.getMethod();
String requestPath = req.getPathInfo();
log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
if (requestPath.endsWith("/")) {
requestPath = requestPath.substring(0, requestPath.length() - 1);
}
ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
log.info("{}", controllerInfo);
if (null == controllerInfo) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
resultRender.invokeController(req, resp, controllerInfo);
}
}
называется в этом классеControllerHandler
иResultRender
Два класса, сначала получите соответствующий метод в соответствии с запрошенным методом и путемControllerInfo
, затем используйтеControllerInfo
После синтаксического анализа соответствующего представления вы можете получить доступ к соответствующей странице или вернуть соответствующую информацию json.
Однако все просьбы, которые были сказаны отDispatcherServlet
После этого вроде не отражается.Это потому что нужно настраивать web.xml.Многие его сейчас используют.spring-bootДрузья могут быть не очень ясны, привыклиspringmvc+spring+mybatisВ эту эпоху приходится писать множество файлов конфигурации, один из которых — web.xml, который необходимо добавить. по подстановочному знаку*
Все запросы выполняются с помощью DispatcherServlet.
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>*</url-pattern>
</servlet-mapping>
Но мы не обязаны этого делать, в честьspring-boot, мы реализуем встроенный Tomcat в следующем разделе и запустим его через лаунчер.
дефект
Возможно, код в этом разделе вызывает у всех неловкость, потому что, хотя функция этого кода уже реализована, структура кода еще нуждается в оптимизации.
во-первыхDispatcherServlet
Это диспетчер запросов, и не должно быть логического кода для обработки Http.
Во-вторых, мы помещаем 3, 4 и 5 шагов MVC в класс, что не есть хорошо, изначально функции каждого шага здесь очень сложные, и эти шаги тоже помещены в класс, так что это не способствует изменению функции соответствующего шага позже.
В настоящее время также не реализована обработка исключений, и страница исключения не может быть возвращена пользователю.
Эти оптимизации будут выполнены в последующих главах.
- Реализация простой Java MVC Framework с нуля (1) — Предисловие
- Реализация простой Java MVC Framework с нуля (2) — Реализация контейнеров компонентов
- Реализация простой Java MVC Framework с нуля (3) — Реализация IOC
- Реализация простой Java MVC Framework с нуля (4) — Реализация АОП
- Внедрение простой Java MVC Framework с нуля (5) — Знакомство с аспектом j для реализации AOP pointcut
- Реализация простой Java MVC Framework с нуля (6) — Усиление функции АОП
- Реализация простой платформы Java MVC с нуля (7) — реализация MVC
- Реализация простой Java MVC Framework с нуля (8) -- Make Starter
- Реализация простой платформы Java MVC с нуля (9) — Оптимизация кода MVC
Адрес источника:doodle
Оригинальный адрес:Реализация простой платформы Java MVC с нуля (7) — реализация MVC