一、servlet的简化
在使用servlet处理前端发来的请求时,往往需要根据不同的请求创建不同的servlet类。可不可以通过一个servlet类处理所有请求呢?答案是可以的:
package com.fan.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//处理所有路由为f的请求
@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
这个servlet类不进行任何请求的处理与响应,而是将请求按照分类并分发给其它对应的类进行处理。创建一个包com.fan.controller,在这个包中添加各种Controller类处理请求,为了举例,这里加入三个Controller类,分别为OrderController、ProductController和UserController。
OrderController
package com.fan.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class OrderController {
public void deleteOrder(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("删除订单!");
}
}
ProductController
package com.fan.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ProductController {
public void onSale(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("It is onSale!");
}
public void offSale(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("It is offSale!");
}
}
UserController
package com.fan.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@FApi("user")
public class UserController {
public void setVip(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("设置VIP等级!");
}
public void disableUser(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("冻结用户!");
}
}
这三个类可以处理订单、商品和用户的操作。比如此时在利用传统请求的方式发送/f/UserController/setVip。这个请求会被MyServlet接收到,此时可以做如下处理:
package com.fan.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String prefix = "com.fan.controller";
//获取地址栏的内容
String uri = req.getRequestURI();
try {
//根据地址栏内容获得应该调用的类和方法
//这里能够获取到类名UserController
String apiP = uri.split("/")[3];
//这里能够获取到方法名setVip
String apiS = uri.split("/")[4];
//根据包名加类名反射,实例化对象
Object obj = Class.forName(prefix + "." + apiP).newInstance();
//继续反射,获取对应的方法
Method[] m = obj.getClass().getDeclaredMethod(apiS);
//调用这个方法,处理请求
m.invoke(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样在前端发出请求后,控制栏应该输出
设置VIP等级!
这样通过一个总路由的分发请求的方式确实可行,相应的controller内类的方法参数列表加上HttpServletRequest req, HttpServletResponse resp,就可以和普通的servlet类一样和前端进行交互了。但是,这种方法也有缺点,那就是类名和方法名有时候太长,也会直接暴露出来,可不可以给方法起个简单的“名字”,前端输入通过这些指定处理请求的方法呢?答案是可以的,可以通过注解来完成。
二、通过注解进一步简化servlet
1、注解的简介
在java中可以通过创建注解类自定义注解,在这里我自定义FApi注解
package com.fan.anno;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
//加入这个注解,可以让我的注解在运行时能够被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface FApi {
//只定义一个字符串变量value
String value();
}
这样之前的三个类和各自的方法都可以使用这个注解起名字,以OrderController为例
package com.fan.controller;
import com.fan.anno.FApi;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@FApi("order")
public class OrderController {
@FApi("del")
public void deleteOrder(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("删除订单!");
try {
resp.getWriter().print("okok");
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、反射注解找到对应方法
注解已经有了,并且每个注解都是独一无二的,那么该如何进行处理前端发过来的注解呢?例如在地址栏输入/order/del,通过反射所有类的所有注解,首先根据order找到对应类,动态创建类,然后反射遍历这个类里面方法的注解,找到与del对应的方法,通过执行这个方法来处理请求:
package com.fan.servlet;
import com.fan.anno.FApi;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String prefix = "com.fan.controller";
//获取地址栏的内容
String uri = req.getRequestURI();
try {
String apiP = uri.split("/")[3];
String apiS = uri.split("/")[4];
//获取部署编译后class文件的路径
String ctrlPath = req.getServletContext().getRealPath("") + "WEB-INF\\classes\\" + prefix.replace(".", "\\");
//遍历该路径获取.class文件
File ctrlDir = new File(ctrlPath);
File[] fs = ctrlDir.listFiles();
for (File f : fs) {
//利用字符串分割获得class的文件名,就是类名。注意不能直接用.分割 而要用\\.分割
String className = f.getName().split("\\.")[0];
//利用类名进行反射,得到类上的注解
Object obj = Class.forName(prefix + "." + className).newInstance();
//获取指定注解及其属性值
FApi fApiP = obj.getClass().getAnnotation(FApi.class);
//匹配一级路径
if (fApiP.value().equals(apiP)) {
//获取所有方法并根据注解进行匹配
Method[] ms = obj.getClass().getDeclaredMethods();
for (Method m : ms) {
FApi fApiS = m.getAnnotation(FApi.class);
if (fApiS.value().equals(apiS)) {
//匹配到方法就可以执行
m.invoke(obj, req, resp);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
因为java文件在编译后文件名不变,所以通过编译路径的.class文件来获得所有类的类名。这样最后在控制台可以输出
删除订单!
在网页上会显示
okok
三、引入mvc
一个总路由通过注解分发请求的方式,其实已经有前人做了总结,并且写成了框架方便我们使用,这里介绍mvc。本次介绍mvc是使用最简单的配置先将mvc运行起来。首先导入需要的jar包,在web项目的WEB-INF文件夹里创建web.xml文件,在文件内写入如下配置:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
因为url-pattern设置为了/所以能够处理整个网站的所有请求。除了这个配置文件,还要在同目录下创建一个名为spring-servlet.xml的配置文件。注意这里的文件名一定要和我写的一样,因为mvc框架在运行的时候,会默认在WEB-INF文件夹内读取spring-servlet.xml文件(当然这个配置文件的名字和路径也可以自定义),本着配置最简单的原则,先这样创建,并且在配置文件中写入:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--controller包扫描-->
<context:component-scan base-package="com.fan.controller"/>
</beans>
这个配置是最简单的配置,有了这个就可以做一些基本的实验了,完整的配置远不止这些。所谓包扫描与之前在编译路径中遍历注解一样。做完简单的配置后,如何处理请求呢?此时已经不需要我们自己写总路由了,只需要写好具体的类和方法就可以了:
package com.fan.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/vip")
public void setVip() {
System.out.println("设置会员!");
}
@RequestMapping("/login")
public ModelAndView login() {
System.out.println("okok");
}
}
解释一下这里注解@RequestMapping("/user"),这个注解就相当于我之前自定义的FApi,里面放入对应“别名”。@Controller表示这个类需要实例化才可以使用里面的方法,所以框架在处理对应请求时会动态创建类,不写这个注解,就会用类名直接调用方法。此时在地址栏输入/user/login,控制栏就会显示如下语句
okok
那么后端如何接受前端传过来的数据呢?比如我在地址栏这样输入/user/login?account=fan&password=123,后端想要接到这些参数有三种方法:
1、参数列表设置对应参数
这里需要注意,参数名一定要和地址栏内传参使用的名称相同
@RequestMapping("/login")
public ModelAndView login(String account,String password) {
System.out.println(String account + "---" + String password);
}
2、创建实体类接受数据
创建一个com.fan.entity,专门存放实体类。在这里,我创建了一个User类
package com.fan.entity;
public class User {
private int id;
private String account;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
这样在方法中可以将实体类作为参数
@RequestMapping("/login")
public void login(User user) {
String account = user.getAccount();
String password = user.getPassword();
System.out.println(account + "---" + password);
}
这样做的好处是:当传入的参数过多时,可以用实体类一一获取,参数列表也可以精简
3、通过HttpServletRequest 获取
类似于我们自己反射的时候,这里也可以将前端的请求和回应作为参数传入方法中,所以类似servlet类,可以接到前端传入的值
@RequestMapping("/login")
public void login(HttpServletRequest req, HttpServletResponse resp) {
String account = req.getParameter("account");
String password = req.getParameter("password");
System.out.println(account + "---" + password);
}
这三种方法都可以获得前端传入的值,所以它们的都会在控制台打印输出
fan---123
后端向前端传输数据,除了可以用(HttpServletRequest req, HttpServletResponse resp,还可以用mvc自带的ModelAndView。下面将利用这个类来处理传统请求,抓取视图返回数据。
1、完善spring-servlet.xml的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--controller包扫描-->
<context:component-scan base-package="com.fan.controller"/>
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/page"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
在这里property name="prefix" value="/page"/ 表示文件路径在page文件夹下,property name="suffix" value=".jsp"表示文件类型为jsp。
2、ModelAndView的使用
@RequestMapping("/login")
public ModelAndView login(User user) {
//构造参数是指抓取的视图的文件名
ModelAndView mav = new ModelAndView("/resA");
System.out.println("okok");
//这个方法相当于在请求域内传入参数
mav.addObject("resA", "登录成功!!!");
return mav;
}
3、在page文件夹下创建视图
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<% String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path + "/";%>
<html>
<head>
<base href="<%=basePath%>">
<title>标题</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous">
<script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.js"></script>
</head>
<body>
<!--获取请求域内的参数-->
<h1>${resA}</h1>
</body>
</html
经过这三步后,在地址栏输入/user/login,后端就会抓取视图,浏览器会跳转页面,并且显示:登录成功!!!
4、对配置文件中bean的详解
注意在后端new ModelAndView("/resA");时,并没有写全视图文件的路径,这是因为有以下配置
<!--class后面是一个带包名的完整类名,当框架读取到这里就会实例化一个InternalResourceViewResolver类-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--property里面是类的属性,其中name是属性名,value是属性值-->
<property name="prefix" value="/page"/>
<property name="suffix" value=".jsp"/>
</bean>
由此我们可以推测,这个InternalResourceViewResolver类里应该有如下代码
public class InternalResourceViewResolver {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
四、spring-servlet.xml文件的自定义
一般会将这个配置文件移动到src下一个config文件夹中,如果这样,web.xml文件的配置就需要更改
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--在servlet标签里添加init-param标签-->
<init-param>
<!--这里一定要是contextConfigLocation-->
<param-name>contextConfigLocation</param-name>
<!--这里是spring-servlet文件路径,classpath指的是编译路径,在部署后java会被编译
此时就会指向编译路径,而文件名也可以在此处自定义,比如改为mvc.xml-->
<param-value>
classpath:/config/spring-servlet.xml
</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--这个标签搭配前面的标签使用,单独配置没有用,改变spring-servlet.xml路径还是会报错-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/config/spring-servlet.xml</param-value>
</context-param>
</web-app>
五、总结
所谓MVC就是指:数据、视图和控制,代表着整个前后端交互的数据传输、视图转换和逻辑控制。其中C是核心,它负责接受传
递处理数据M,控制视图C的的渲染。这是一种界面层交互设计方式,也是一种思想,并不是只有在java中存在,也可以扩展到
其它语言其他领域。