SpringMVC: передний контроллер

MVC

В SpringMVC разработчики не нужно заботиться о механизме сервлета и других компонентов. Им нужно только следить за конвенциями SpringMVC (использование рамок): добавить методы в контроллере и объявить запросы, которые можно обработать, сохранить данные в Модель и вернуть вид.,

SpringMVC инкапсулирован в J2EE, что позволяет разработчикам больше сосредоточиться на бизнесе.В J2EE только фильтр и сервлет могут обрабатывать запросы.Поскольку фильтр более склонен проверять обработку (например, законность запросов и т. д.), обработка бизнес-запросов должна быть завершен сервлетом.В SpringMVC разработчикам не нужно реализовывать сервлет для обработки запросов.Давайте проанализируем механизм реализации SpringMVC.

1. Что такое фронт-контроллер

При использовании J2EE-разработки для обработки каждого запроса требуется сервлет, и весь процесс можно смоделировать как процесс обращения пациента за медицинской помощью в больницу Каждый пациент, направляющийся в больницу, эквивалентен запросу, поэтому сервлет, который обрабатывает этот запрос доктор.

Когда вы идете в небольшую клинику, вы можете пойти прямо в кабинет врача, чтобы пройти процедуры, а затем вы можете получить лечение.Каждый врач знает, как проходить процедуры для пациентов.Но для большой больницы, потому что есть много пациенты и отделения, пусть каждый врач изучит эти несущественные предметы. Профессиональный процесс - пустая трата ресурсов. В крупных больницах будет создана стойка регистрации, и стойка регистрации будет отвечать за обработку процедур для пациентов. После завершения процедур, он будет передан врачам разных специальностей для диагностики и лечения в соответствии с состоянием пациента.При присоединении вам необходимо заполнить соответствующую информацию об изменении в едином формате.Ресепшн регулярно собирает и организует, чтобы вы может точно понять информацию о посещении врача в день.В то же время врачи одной дисциплины располагаются в одном кабинете, а посещающие врачи располагаются в относительно централизованном месте. собирать информацию о посещении врача, а также это удобно для управления врачом.

Когда все пациенты приходят в больницу для лечения, они равномерно принимаются на стойке регистрации и проходят формальности.Согласно собранной информации о посещении врача, их направляют к соответствующему врачу для диагностики и лечения.Через стойку регистрации, реализовано единое управление лечебным процессом пациента.

SPRINGMVC обрабатывается в соответствии с вышеуказанными идеями, и у него есть такая передняя стойкапередний контроллер.

2. Поток обработки фронт-контроллера

Приведенная выше копия преобразуется в описание SpringMVC:

Каждый метод контроллера (Врач) в запросе, который может быть обработан (Укажите заболевание, которое можно диагностировать).Передний контроллер (стойка регистрации) на работе каждый день (Каждый раз, когда проект начинается), сходи к врачу (Контроллер каталога) каждого отдела (контроллер)собирать(нагрузка) каждый врач (метод контроллера) Может ли диагностика и лечение болезни (Обработка запроса), собранные и организованные в документы (Сопоставление методов и URL-адресов).Когда пациент (клиент) обратиться за медицинской помощью (послать запросвремя) администратором (Фронт-контроллер обрабатывает все запросы), стойка регистрации зависит от состояния пациента (URL-адрес запроса доступа) из собранного документа (Mapping) найти врача, который может диагностировать и лечить болезнь (метод контроллера) И передан (распределение) соответствующий врач для диагностики и лечения (Выполнять бизнес-логику).

Видно, что фронт-контроллер — это сервлет, отвечающий за обработку всех запросов.

3. Реализация фронт-контроллера

Выше был объяснен принцип реализации интерфейсного контроллера SpringMVC.Следующий код реализует интерфейсный контроллер.

3.1 каталог контроллера конфигурации (обозначенный врач офисной зоны):

В крупной системе могут быть сотни или тысячи классов, и если интерфейсный контроллер будет сканировать все классы при загрузке контроллера, это серьезно повлияет на скорость загрузки, поэтому мы должны сообщить интерфейсному контроллеру конкретную контроллер, который необходимо сканировать.Каталог, то есть параметры конфигурации внешнего контроллера необходимы.Таким образом, интерфейсному контроллеру нужно только пройти классы в указанном каталоге при сканировании.

Распространенным способом является определение фронт-контроллера (web.xmlНастройте параметры инициализации при настройке сервлета).Однако, поскольку функции фреймворка продолжают увеличиваться, во фронт-контроллере будет все больше и больше элементов конфигурации, что не является гибким.Поэтому требуется более гибкий метод: через конфигурацию XML, когда загружается фронт-контроллер, каталог, в котором находится контроллер, получается путем чтения XML-файла конфигурации и его разбора.

Формат XML каталога контроллера конфигурации следующий:controllerУзел — это конфигурация, связанная с контроллером,packageСвойство — это каталог, в котором находится контроллер.

<mvc>
    <controller package="com.atd681.xc.ssm.controller" />
</mvc>

3.2 Аннотация сопоставления URL (заполнить форму болезней, которые можно диагностировать и лечить)

Определяет метод контроллера для установки аннотаций, которые могут обрабатывать запрос.

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

    // 请求URL
    String url();

    // HTTP方法
    String method() default "GET";

}

Методы контроллера единообразно используют аннотации для объявления запросов, которые могут быть обработаны.

public class UserController {

    // 处理"/list"请求
    @RequestMapping(url = "/list")
    public String userList() {
        return "";
    }

    // 处理"/detail"请求
    @RequestMapping(url = "/detail")
    public String userDetail() {
        return "";
    }

}

3.3 Внедрение интерфейсного контроллера (стойка регистрации в больнице)

3.3.1 Загрузить конфигурационный файл при старте проекта (каждый день ресепшн выходит на работу)

В J2EE оговорено, что сервлет будет выполнять метод init при загрузке, поэтому мы можем прописать процесс загрузки контроллера в методе init.

В соответствии с принципом соглашения по настройке: мы соглашаемся с тем, что путь к файлу конфигурации находится в пути к классам, а имя - mvc.xml. В некоторых сценариях пользователям необходимо настроить путь и имя файла конфигурации, поэтому мы также необходимо поддерживать пользовательские, настраиваемые При настройке пути и имени файла вweb.xmlчерез имяconfigLocationПараметры передаются во фронт-контроллер.

/**
 * 前端控制器(负责处理所有请求)
 */
public class DispatcherServlet extends HttpServlet {

    // 默认MVC配置文件路径
    private static final String DEFAULT_CONFIG_LOCATION = "mvc.xml";

    /**
     * 初始化Servlet. 容器初始化Servlet时调用, 加载配置文件初始化MVC相关组件(控制器,视图解析器等)
     */
    @Override
    public void init() throws ServletException {

        // 获取用户自定义的配置文件路径
        String configLocation = getInitParameter("configLocation");

        // 未定义配置文件路径, 使用默认配置文件路径
        if (configLocation == null || "".equals(configLocation)) {
            configLocation = DEFAULT_CONFIG_LOCATION;
        }

        try {

            // 开始加载配置文件(JDom解析XML)
            String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
            Document doc = new SAXBuilder().build(new File(classpath, configLocation));

            // 解析配置文件中控制器的配置
            initController(doc);

        } catch (Exception e) {
            throw new ServletException("加载配置文件错误", e);
        }

    }
	
}

3.3.2.Загрузить контроллер (зайти в каждое отделение в офисной зоне, чтобы собрать бланк заболеваний, которые врачи могут диагностировать и лечить)

Определите глобальные переменные во внешнем контроллере, чтобы сохранить отношение сопоставления между методами контроллера и URL-адресами, в дальнейшем называемымиurlMapping.

// URL映射MAP(K:URL, V:对应的控制器方法)
private static Map<String, Method> urlMappings = new HashMap<String, Method>();;

Получите каталог контроллера в файле конфигурации и пересекайте каталог, получите имя файла каждого контроллера. Загрузите контроллер Java и сохраните каждый метод и соответствующий URL к сопоставлению URLMapping.

/**
 * 解析配置文件中的控制器配置
 * 
 * @param doc XML配置文件
 * @throws Exception
 */
@SuppressWarnings("unchecked")
private void initController(Document doc) throws Exception {

    // 配置格式:<controller package="com.atd681.xc.ssm.controller"/>
    // package为控制器所在目录, 模拟SpringMVC配置文件中的控制器包扫描
    List<Element> controllerEle = doc.getRootElement().getChildren("controller");
    if (controllerEle == null) {
        throw new Exception("请配置Controller节点.");
    }

    // 获取配置文件中的控制器所在目录
    String controllerPackage = controllerEle.get(0).getAttributeValue("package");
    if (controllerPackage == null) {
        throw new Exception("Controller的package属性必须设置.");
    }

    // 获取控制器目录的在磁盘中的绝对路径(D:\atd681-xc-ssm\com\atd681\controller)
    // Java目录分隔符需转换为文件系统格式(com.atd681 -> com/atd681)
    String controllerDir = controllerPackage.replaceAll("\\.", "/");
    String controllerPath = getClass().getClassLoader().getResource(controllerDir).getPath();

    // 遍历控制器目录下的所有CLASS
    for (File controller : new File(controllerPath).listFiles()) {

        String className = controller.getName().replaceAll("\\.class", ""); // 控制器类名称
        Class<?> clazz = Class.forName(controllerPackage + "." + className); // 加载控制器类

        // 遍历控制器类的所有方法,将每个方法和处理的URL做映射
        for (Method method : clazz.getMethods()) {

            // 只处理有@RequestMapping注解的方法
            if (!method.isAnnotationPresent(RequestMapping.class)) {
                continue;
            }

            RequestMapping rm = method.getAnnotation(RequestMapping.class);

            // 同一URL可能以GET或POST提交, URL和HTTP方法(GET/POST)才能确定是相同的请求
            // 将URL和HTTP方法作为KEY, 使用统一方法生成KEY便于在分发时准确的获取对应的方法
            String urlKey = wrapperKey(rm.url(), rm.method());

            // 当多个方法同时声明了相同的请求时, 在前端控制器分发时无法准确的找到对应方法
            if (urlMappings.containsKey(urlKey)) {
                throw new Exception("URL不能重复, URL: " + rm.url());
            }

            // 保存URL及对应的方法
            urlMappings.put(urlKey, method);

        }

    }

}
  • Когда множественные методы настраиваются с тем же URL, передний интерфейс контроллер не сможет отличить этот запрос должен быть обработан при получении запроса, поэтому не более двух способов объявить тот же URL, когда контроллер Требуется загруженная проверка повторяемости URL.

  • HTTP поддерживает несколько методов, таких как GET/POST, для запроса одного и того же URL. Пример: запрос метода GET./addОт имени посещения страницы добавления запрос метода POST/addПредставитель отправляет данные, так как два запроса разные, два метода нужно обрабатывать отдельно, поэтому для настройки обработки необходимо два метода/addзапрос сHTTP MethodДифференциация (GET & POST).Метод настройки управленияurlMappingНужно использоватьURL+HTTP MethodВ качестве ключа.Он также должен быть основан на распределении запросов.URL+HTTP MethodНайдите соответствующий метод обработки как KEY.

Инкапсуляция создания унифицированных правилurlMappingКЛЮЧ, в настройкахurlMappingИспользуйте тот же КЛЮЧ, что и при раздаче.

/**
 * 封装URL映射的KEY,在加添加映射和分发时使用相同的KEY
 * 
 * @param url
 * @param method
 * @return url|GET
 */
private String wrapperKey(String url, String method) {
    return url.toLowerCase() + "|" + method.toUpperCase();
}

3.3.3 Настройка фронт-контроллера

существуетweb.xmlНастройте фронт-контроллер для обработки всех запросов в , и укажите путь к файлу конфигурации.

<servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class>com.atd681.xc.ssm.framework.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>configLocation</param-name>
        <param-value>/com/atd681/xc/ssm/framework/mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

На этом этапе фронт-контроллер будет загружен при запуске проекта, и все методы обработки и URL-адреса обработки будут сопоставлены один за другим и сохранены.

3.4. Распространение запросов

В J2EE предусмотрено, что сервисный метод будет выполняться, когда сервлет обработает запрос, поэтому логику распределения нужно прописать в сервисном методе.urlMappingНайдите соответствующий метод в и вызовите метод динамически с помощью отражения JAVA.

Обычно контроллер в проекте вызывает уровень бизнес-логики и DAO или другие интерфейсы.Неизвестные ошибки-исключения неизбежно возникают в процессе вызова.Если информация об исключении не обрабатывается в программе, эти исключения будут возвращены на передний план. Конечный контроллер. Следовательно, эти исключения должны быть перехвачены, чтобы пользователи могли обрабатывать их единообразно. Также известен как глобальный механизм обработки исключений.

Определите метод doService и выполните логику распределения в этом методе.Вызовите doService в методе службы, чтобы выполнить распределение и перехватить все исключения, которые он выдает, для достижения унифицированной обработки исключений.

/**
 * 处理所有请求
 */
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    try {
        doService(req, resp);
    }
    catch (Exception e) {
        // 可以在这里捕获异常进行全局处理
        // 模拟Spring全局异常处理
        e.printStackTrace();
    }

}

Запрос отправляется в методе doService.

/**
 * 根据请求URL分发至控制器
 */
private void doService(HttpServletRequest req, HttpServletResponse resp) throws Exception {

    // 获取当前请求对应的方法名称
    // urlMappings保存每个URL对应的处理方法, 详见init方法
    Method method = urlMappings.get(getMappingKey(req));

    // 未找到方法处理时跳转至全局错误页
    // 此处忽略该过程直接跑出异常
    if (method == null) {
        throw new RuntimeException("没有找到处理该请求的方法");
    }

    // 实例化控制器
    Object classInstance = method.getDeclaringClass().newInstance();
    // 通过Java反射调用控制器方法
    method.invoke(classInstance, new Object[] {});

}
  • При распространении он должен основываться на запрошенном URL иHTTP Method(GET & POST) как КЛЮЧ отurlMappingНайдите соответствующий метод обработки в .Правила формирования КЛЮЧА нужно и задайтеurlMappingПравила сгенерированного KEY непротиворечивы (унифицированный вызовwrapperKeyметод).
  • Из-за другой конфигурации сервера веб-приложений некоторые проекты необходимо добавлять в URL-адрес при доступе к имени проекта.Когда доступный URL-адрес содержит имя проекта, соответствующий метод обработки не может быть найден (urlMappingURL-адрес не содержит названия проекта. В этом случае вам необходимо использовать URL-адрес после удаления названия проекта.
/**
 * 根据Request取得访问地址对应的处理方法KEY
 * 
 * <pre>
 * 例: 请求路径/list, get请求. 对应的key为"/list|get"
 * 如果请求路径中含有项目名称,去掉项目名称, 例请求为:/demo/list,转换为/list
 * </pre>
 * 
 * @param req
 * @return
 */
private String getMappingKey(HttpServletRequest req) {
    
    String httpMethod = req.getMethod().toUpperCase(); // HTTP Method(GET&POST)
    String httpUrl = req.getRequestURI().toLowerCase(); // 请求的URL

    // 由于WEB服务器配置不同, 有些项目访问时需要在URL中加入项目名称
    // 如果访问的URL中含有项目名称,将项目名称从URL中去除
    if (httpUrl.startsWith(req.getContextPath())) {
        httpUrl = httpUrl.replaceFirst(req.getContextPath(), "");
    }

    // 生成KEY的规则应和加载控制器时生成KEY的规则相同
    return wrapperKey(httpUrl, httpMethod);
    
}

Пока мы реализовали механизм распределения фронт-контроллера, при поступлении запроса фронт-контроллер будет распределять запрос на соответствующий метод контроллера для обработки.

3.5. Привязка параметров запроса:

Метод контроллера в приведенном выше примере не имеет параметров. Во многих сценариях методу контроллера требуются параметры (запрос, ответ, модель и параметры запроса и т. д.). SpringMVC предоставляет гибкий механизм привязки параметров.

SpringMVC может привязывать параметры, переданные в запросе, к параметрам соответствующего метода контроллера.Параметры могут быть базовыми типами данных или пользовательскими Javabeans.В качестве примера возьмем следующий URL-адрес, запрос содержит 3 параметра.

http://localhost/list?userName=zhangsan&age=30&gender=M

Контроллер использует для приема 3 параметра, причем положение параметров может быть произвольным

@RequestMapping("/list")
public String list(String userName, Integer age, String gender) {
}

@RequestMapping("/list")
public String list(Integer age, String gender, String userName) {
}

Когда есть много параметров, Javabean может быть определен для получения

public class User {

    private String userName;
    private Integer age;
    private String gender;

    // Getter&Setter
    
}
@RequestMapping("/list")
public String list(User user) {
    
}

Если вам нужен объект Request, просто добавьте его в параметры метода контроллера, при этом порядок параметров не ограничен.

@RequestMapping("/list")
public String list(HttpServletRequest req, User user) {
}
@RequestMapping("/list")
public String list(User user, HttpServletRequest req) {
}

Давайте проанализируем, как SpringMVC достигает такой гибкой привязки параметров.Параметры в методе контроллера можно разделить на несколько категорий:

  • Объекты, которые приходят с запросом: например.Request, Response, SessionЖдать.
  • Объекты, определенные платформой: такие как ModelMap, которые содержат данные.
  • Модель получения параметров запроса: Различные формы, вы можете использовать несколько основных типов параметров или прием JavaBean.

Передний контроллер при запрашивании контроллера распределения уже приобрел соответствующий метод, также может приобрести тип аргумента метода. Каждый из этих параметров в соответствии с их типом, чтобы найти объект, соответствующий назначению, нам нужно определить параметр типа в соответствии с типом параметра:

  • Объект, который приходит с запросом: Просто назначьте объект объекта в J2EE.
  • Объекты, определенные фреймворком: присваивайте значения после создания экземпляров соответствующих объектов.
  • Все параметры, отличные от двух вышеуказанных категорий, считаются используемыми для получения параметров запроса.

Основываясь на приведенном выше анализе, параметры метода контроллера динамически связаны.

// 自定义的ModelMap, 保存在此的数据便于在视图中使用
Map<String, Object> model = new HashMap<String, Object>();

// 处理请求的控制器方法参数类型数组
Class<?>[] classes = method.getParameterTypes();
// JAVA反射调用方法时需要传入参数的实例化对象数组
Object[] methodParams = new Object[classes.length];

// 遍历控制器方法的某个参数, 根据参数类型设置相应参数或其实例
// 控制器方法的参数位置变化时, 此处设置的参数的实例化对象数组位置也随之变化
for (int i = 0; i < classes.length; i++) {
    Class<?> paramClass = classes[i];

    if (paramClass.isAssignableFrom(HttpServletRequest.class)) {
        methodParams[i] = req; // 将J2EE的Request对象设置到参数
    }
    else if (paramClass.isAssignableFrom(HttpSession.class)) {
        methodParams[i] = req.getSession(); // 将J2EE的会话对象设置到参数
    }
    else if (paramClass.isAssignableFrom(Map.class)) {
        methodParams[i] = model; // 将自定义保存数据的Map设置到参数
    }
    else {
        // 其余的类型的参数为接收请求参数, 实例化该参数并将设置请求数据
        methodParams[i] = wrapperBean(req, paramClass);
    }

}

Если имя параметра в запросе согласуется с именем свойства Javabean, данные параметра устанавливаются в свойство Javabean через механизм отражения JAVA.

/**
 * 将请求中的参数分别设置到Javabean对应的属性中
 */
@SuppressWarnings({ "unchecked" })
private <T> T wrapperBean(HttpServletRequest req, Class<?> bean) {

    T beanInstance = null;

    // 实例化处理方法中从参数bean
    try {
        beanInstance = (T) bean.newInstance();
    }
    catch (Exception e) {
        throw new RuntimeException("请求参数映射出现错误", e);
    }

    // 请求中所有参数
    Set<String> keySet = req.getParameterMap().keySet();

    // 遍历请求中的参数将值设置到Javabean对应的属性中
    for (String reqParam : keySet) {

        try {
            Class<?> fieldType = bean.getDeclaredField(reqParam).getType(); // Bean中参数类型
            Object fieldValue = getRequestValue(req, reqParam, fieldType); // Bean中参数在请求中的值
            String fieldSetter = "set" +  reqParam.substring(0, 1).toUpperCase() + reqParam.substring(1); // Bean中参数的set方法

            // 使用属性的Setter方法将请求中的值设置到属性中
            bean.getMethod(fieldSetter, fieldType).invoke(beanInstance, fieldValue);

        }
        catch (Exception e) {
            // BEAN中没有请求中对应的参数属性时继续下一个参数处理
            // JAVA反射未找到类的属性时会抛出异常终止循环
        }

    }

    return beanInstance;

}

Данные параметров, полученные в запросе, имеют строковый тип и должны быть преобразованы в тип соответствующего атрибута Javabean.

/**
 * 将request属性值转换为对应JavaBean属性类型
 */
private Object getRequestValue(HttpServletRequest req, String name, Class<?> type) {
    String value = req.getParameterValues(name)[0];

    if (Integer.class.isAssignableFrom(type)) {
        return Integer.valueOf(value);
    }
    else if (Long.class.isAssignableFrom(type)) {
        return Long.valueOf(value);
    }
    else if (BigDecimal.class.isAssignableFrom(type)) {
        return BigDecimal.valueOf(Long.valueOf(value));
    }
    else if (Date.class.isAssignableFrom(type)) {
        try {
            return new SimpleDateFormat().parse(value);
        }
        catch (ParseException e) {
            throw new RuntimeException("参数[name]格式不正确");
        }
    }
    return value;
}

4. Резюме

Фронт-контроллер написан, запускаем проект:

  • просить/list: воплощать в жизньUserController.userList
  • просить/detail: воплощать в жизньUserController.userDetail
  • проситьlist?userName=zhangsan&age=30&gender=M, установит три параметра в запросе наUserController.userListв параметрах метода.

Основная идея фронт-контроллера заключается в механизме распределения и привязке параметров.Он экранирует избыточный код J2EE для разработчиков и повышает эффективность разработки.Это позволяет разработчикам больше сосредоточиться на развитии бизнеса.Широко используется в процессе реализации фронт-контроллера. Механизм отражения JAVA используется для реализации динамической обработки. Разработчики, не знакомые с отражением JAVA, должны закрепить соответствующие точки знаний.

В следующей статье будет проанализирован механизм разрешения представления SpringMVC на основе шаблона стратегии.