以下内容转载和参考自:廖雪峰的官方网站。
1、JSP
前面的Servlet开发是在HTTP处理中编写HTML代码,将要显示的变量值嵌入到HTML文本中,如果要开发一个复杂的网页的话这样编写HTML就很麻烦。我们可以专门编写一个保存的HTML文件,与普通的HTML文件不同的是可以在其中使用HttpServletRequest等HTTP对象,这个文件就是JSP。
JSP是Java Server Pages的缩写,文件名必须以.jsp结尾,整个文件与HTML并无太大区别,但在其中可以插入Java代码,如下所示为hello.jsp的内容,包含在<%......%>之间的是Java代码,使用<%= xxx %>则可以快捷输出一个变量的值,包含在<%--和--%>之间的是注释。其中的out是JSP的内置变量,表示HttpServletResponse的PrintWriter,request表示HttpServletRequest对象,其它的还有session,表示当前HttpSession对象。
Hello World - JSP
<%-- JSP Comment --%>
Hello World!
<%
String str = "Your IP address is ";
out.println(str);
%>
<%= request.getRemoteAddr() %>
将上面的hello.jsp放到Tomcat目录webapps的ROOT目录中的话,就可以在浏览器中输入http://localhost:8080/hello.jsp来访问这个JSP页面,如下所示,如果放到webapps下的hello目录中的话那么应该使用http://localhost:8080/hello/hello.jsp。实际上JSP在执行前会被编译成一个Servlet,可见JSP本质上就是一个Servlet,只不过无需配置映射路径,在Tomcat服务运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。可以看出,Servlet是在Java代码中嵌入输出HTML,而JSP则是在HTML中嵌入动态输出,比如嵌入Java代码,所以JSP是以Java语言作为脚本语言的。JSP目前已经很少使用。
除了<% ... %>外,JSP页面还包含一些其它的指令,如page指令引入Java类来使用简单类名而不是完整类名,使用include指令可以引入另一个JSP文件:
<%@ page import="java.io.*" %>
<%@ page import="java.util.*" %>
<%@ include file="header.jsp"%>
Index Page
<%@ include file="footer.jsp"%>
2、MVC
可以只在Servlet中调用Java代码来处理数据,然后将处理好的数据交给jsp来进行渲染展示,各自负责自己擅长的工作,这样符合MVC模式,下面的UserServlet作为控制器(Controller),User作为模型(Model),user.jsp作为视图(View),整个MVC架构如下:
package xsl;
public class User {
public User(String name, String schoolName, String address){
this.name = name;
this.schoolName = schoolName;
this.address = address;
}
public String name;
public String schoolName;
public String address;
}
import xsl.User;
@WebServlet(urlPatterns = "/test")
public class UserServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
User user = new User("Bob", "No.1 Middle School", "101 South Street");
req.setAttribute("user", user); // 将数据放入到请求中
req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp); //将请求发送给jsp来进行处理渲染
}
}
下面的user.jsp要放到Tomcat中对应的/WEB-INF/目录下,这里通过请求获取User数据
,然后在页面中直接输出(此处未考虑HTML的转义问题,有潜在安全风险):
<%@ page import="xsl.User"%>
<%
User user = (User) request.getAttribute("user");
%>
Hello World - JSP
Hello <%= user.name %>!
School Name:
<%= user.schoolName %>
School Address:
<%= user.address %>
3、Spring MVC
对于上面的MVC模式,还可以进一步的降低耦合度,如将控制器(Controller)从HttpServlet中剥离出来,在HttpServlet中调用控制器中的相关方法来实现对浏览器请求的处理。如下所示的UserController就是一个控制器,其signin()方法会在浏览器请求http://localhost:8080/signin的时候被Servlet服务调用(Servlet根据请求的路径"/signin"来找到注解值为"/signin"的控制器方法来调用),signout()、profile()同理也是。
//使用GetMapping和PostMapping注解来判断控制器中方法对应POST还是GET,通过注解中的值来确定调用控制器中的哪个方法
//GetMapping注解,用来声明控制器中所有GET方法,其值为对应的Servlet映射路径
public @interface GetMapping
{
String value();
}
//PostMapping注解,用来声明控制器中所有POST方法,其值为对应的Servlet映射路径
public @interface PostMapping
{
String value();
}
public class UserController {
@GetMapping("/signin") //自定义注解GetMapping,表明这是一个GET请求处理方法,注解值表明了该方法能够处理的路径
public ModelAndView signin() {
//...
}
@PostMapping("/signin") //自定义注解PostMapping,表明这是一个POST请求处理方法,注解值表明了该方法能够处理的路径
public ModelAndView doSignin(SignInBean bean) {
//...
}
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
//...
}
@GetMapping("/test")
public ModelAndView testFun(int num) {
//...
}
@GetMapping("/profile")
public ModelAndView profile(HttpServletResponse response, HttpSession session) throws IOException {
User user = (User) session.getAttribute("user");
if (user == null) {
return new ModelAndView("redirect:/signin"); // 未登录,跳转到登录页:
}
if (!user.isManager) {
response.sendError(403); // 权限不够,返回403:
return null;
}
String viewFileName = "/profile.html";
Map modelData = Map.of("user", user);
return new ModelAndView("/profile.html", modelData);
}
}
如下所示的DispatcherServlet是Servlet服务,在初始化中就将控制器UserController的成员方法保存到了mapGetMappings(GET请求)和mapPostMappings(POST请求)中,这样Servlet服务根据请求的路径来获得对应的控制器中的处理方法来调用。可以看到,使用反射的话Servlet服务无需知道控制器中的方法名称,通过方法的注解就可以获得指定的方法,而且我们能够自由的增加或者删除控制器中的方法,Servlet服务不用修改代码通过反射就能够获取控制器中的所有方法。
public class User {
public int id;
public String name;
public boolean isManager;
}
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet{
private UserController controller;
private Map mapGetMappings = new HashMap<>(); //GET请求
private Map mapPostMappings = new HashMap<>(); //POST请求
private ViewEngine viewEngine; //模板引擎
@Override
public void init() throws ServletException {
controller = new UserController(); //控制器对象实例
scanInControllers(); //通过反射获得控制器中方法到mapGetMappings、mapPostMappings
viewEngine = new ViewEngine(getServletContext()); //getServletContext()是HttpServlet中成员方法
}
private void scanInControllers(){
try {
for(Method m : Class.forName("UserController").getMethods()) //获得UserController类的所有方法
{
String strAnnotationValue = null; //注解中的值:路径,如"/signin"
if(m.isAnnotationPresent(GetMapping.class)){ //方法是否包含GetMapping注解
GetDispatcher dispatcher = new GetDispatcher(controller, m, m.getParameterNames(), m.getParameterTypes());
strAnnotationValue = m.getAnnotation(GetMapping.class).value();
mapGetMappings.put(strAnnotationValue, dispatcher);
}
else if(m.isAnnotationPresent(PostMapping.class)){ //方法是否包含PostMapping注解
PostDispatcher dispatcher = new PostDispatcher(controller, m, m.getParameterTypes());
strAnnotationValue = m.getAnnotation(PostMapping.class).value();
mapPostMappings.put(strAnnotationValue, dispatcher);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
String strURI = req.getRequestURI(); //eg: "/hello"
String strContext = req.getContextPath(); //上下文,即war包名,ROOT包的话该值为空
String path = strURI.substring(strContext.length()); //eg: "/hello"
GetDispatcher dispatcher = mapGetMappings.get(path); // 根据路径查找对应的控制器中的方法
if (dispatcher == null) {
resp.sendError(404); // 未找到返回404:
return;
}
ModelAndView mv = null;
try {
mv = dispatcher.invoke(req, resp); // 调用Controller对应的方法获得ModelAndView
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (mv == null) { //返回null表示内部已自行处理完毕,直接返回
return;
}
if (mv.view.startsWith("redirect:")) { // 以"redirect:"开头的view表示重定向
resp.sendRedirect(mv.view.substring(9));
return;
}
// 将模板引擎渲染的内容写入响应:
PrintWriter pw = resp.getWriter();
this.viewEngine.render(mv, pw);
pw.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
......
}
}
我们将一个方法保存到一个GetDispatcher/PostDispatcher中,调用Dispatcher的invoke()可以调用对应的方法。比如signout(HttpSession session)方法需要的参数是HttpSession对象,所以我们就获取当前请求的HttpSession来传给它,testFun()方法需要一个名为num的int对象,那么就从当前请求中查找名为num的参数来传给testFun():
public class GetDispatcher {
Object instance; // Controller控制器实例
Method method; // Controller中的方法
String[] parameterNames; // 方法参数名称
Class>[] parameterClasses; // 方法参数类型
public GetDispatcher(Object instance, Method method, String[] parameterNames, Class>[] parameterClasses){
this.instance = instance;
this.method = method;
this.parameterNames = parameterNames;
this.parameterClasses = parameterClasses;
}
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws InvocationTargetException, IllegalAccessException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
String parameterName = parameterNames[i]; //参数名
Class> parameterClass = parameterClasses[i]; //参数类型
if (parameterClass == HttpServletRequest.class) { //参数类型是HttpServletRequest
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) { //参数类型是HttpServletResponse
arguments[i] = response;
} else if (parameterClass == HttpSession.class) { //参数类型是HttpSession
arguments[i] = request.getSession();
} else if (parameterClass == int.class) { //参数类型是int,查找URL参数中是否包含该int
arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == long.class) { //参数类型是long,查找URL参数中是否包含该long
arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == boolean.class) { //参数类型是boolean,查找URL参数中是否包含该boolean
arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
} else if (parameterClass == String.class) { //参数类型是String,查找URL参数中是否包含该String
arguments[i] = getOrDefault(request, parameterName, "");
} else {
throw new RuntimeException("Missing handler for type: " + parameterClass);
}
}
return (ModelAndView) this.method.invoke(this.instance, arguments);
}
private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
String s = request.getParameter(name); //请求中是否包含该名称的参数
return s == null ? defaultValue : s;
}
}
和GET请求不同,POST请求严格地来说不能有URL参数,所以对于处理post请求的方法,HttpServletRequest、HttpServletResponse、HttpSession类型的参数照样从当前请求中获得传给该方法,其它类型的参数应当从当前请求的Body中读取数据传入。这里我们为了简化处理,只支持携带JSON格式数据的POST请求,这样把Post数据转化为JavaBean就非常容易。所以目前Controller中的POST请求处理方法的参数类型只支持HttpServletRequest、HttpServletResponse、HttpSession和JavaBean,如doSignin()方法。
public class PostDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
Class>[] parameterClasses; // 方法参数类型
ObjectMapper objectMapper; // 解析JSON数据
public PostDispatcher(Object instance, Method method, Class>[] parameterClasses){
this.instance = instance;
this.method = method;
this.parameterClasses = parameterClasses;
objectMapper = new ObjectMapper();
}
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, InvocationTargetException, IllegalAccessException {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
Class> parameterClass = parameterClasses[i]; //参数类型
if (parameterClass == HttpServletRequest.class) { //参数类型是HttpServletRequest
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) { //参数类型是HttpServletResponse
arguments[i] = response;
} else if (parameterClass == HttpSession.class) { //参数类型是HttpSession
arguments[i] = request.getSession();
} else { //参数类型是JavaBean
// 读取Body中的JSON并解析为JavaBean
BufferedReader reader = request.getReader();
arguments[i] = this.objectMapper.readValue(reader, parameterClass); //通过Jackson的objectMapper将JSON数据转换为JavaBean
}
}
return (ModelAndView) this.method.invoke(instance, arguments);
}
}
可以看到,控制器中的方法都返回的是ModelAndView类型,它包含了要使用的html文件(类似jsp文件的功能)路径以及model数据。我们在servlet服务中可以将控制器返回的ModelAndView交给模板引擎来处理,模板引擎可以将html文件渲染好model数据后将内容写入到应答。我们还硬性规定模板必须放在webapp/WEB-INF/templates
目录下。
public class ModelAndView {
ModelAndView(String view){
this.view = view;
}
ModelAndView(String view, Map model){
this.view = view;
this.model = model;
}
Map model; //数据
String view; //html文件地址
}
public class ViewEngine {
private final PebbleEngine engine;
public ViewEngine(ServletContext servletContext) { //创建模板引擎
// 定义一个ServletLoader用于加载模板
ServletLoader loader = new ServletLoader(servletContext);
loader.setCharset("UTF-8"); // 模板编码:
loader.setPrefix("/WEB-INF/templates"); // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录
loader.setSuffix(""); // 模板后缀
// 创建Pebble实例:
this.engine = new PebbleEngine.Builder()
.autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
.cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
.loader(loader).build();
}
public void render(ModelAndView mv, Writer writer) throws IOException {
// 使用模板引擎进行渲染
PebbleTemplate template = this.engine.getTemplate(mv.view);
template.evaluate(writer, mv.model);
}
}
JSP对页面开发不友好,所以我们使用模板引擎(又称ViewResolver,视图解析器)来处理HTML和数据,有很多开源的模板引擎,比如Thymeleaf、FreeMarker、Velocity、Pebble,这里使用的是Pebble模板引擎,它使用Jinja语法,特点是语法简单,支持模板继承,编写出来的模板类似下面的内容,即变量用{{ xxx }}表示,控制语句用{% xxx %}表示:
{% for user in users %}
- {{ user.username }}
{% endfor %}
我们还硬性规定静态文件必须放在webapp/static
目录下,因此,为了便于开发,我们还顺带实现一个FileServlet
来处理静态文件的请求:
@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
class FileServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext ctx = req.getServletContext();
String strContextPath = ctx.getContextPath(); // 获取上下文路径,如"/warName",ROOT包的话为空
String urlPath = req.getRequestURI(); //获取RequestURI,如"/warName/hello.ico",ROOT包的话为"/hello"
String path = urlPath.substring(strContextPath.length()); // RequestURI包含ContextPath,需要去掉,所以path为"/hello.ico"
String filepath = ctx.getRealPath(path); // 获取真实文件路径
if (filepath == null) { // 无法获取到路径
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path path = Paths.get(filepath);
if (!path.toFile().isFile()) { // 文件不存在
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String mime = Files.probeContentType(path); // 根据文件名猜测Content-Type
if (mime == null) {
mime = "application/octet-stream";
}
resp.setContentType(mime);
// 读取文件并写入Response
OutputStream output = resp.getOutputStream();
try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
input.transferTo(output);
}
output.flush();
}
}
为了把方法参数的名称编译到class文件中,以使能通过反射获得方法的参数名,需要打开编译器的一个参数,在Eclipse中勾选Preferences-Java-Compiler-Store information about method parameters (usable via reflection);在Idea中选择Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters,填入-parameters;在Maven的pom.xml添加一段配置如下:
4.0.0
...
org.apache.maven.plugins
maven-compiler-plugin
-parameters
经过上面的一番调整后,整个工程实际上是 DispatcherServlet(Servlet)+ UserController(Controller)+ GetDispatcher/PostDispatcher(连接Servlet和Controller:保存Controller中方法给Servlet使用)+ ModelAndView(保存Model和View)+ ViewEngine(模板引擎(视图解析器),用来处理ModelAndView)。业务流程是DispatcherServlet保存了UserController中的相关处理方法到GetDispatcher/PostDispatcher,当DispatcherServlet收到浏览器的请求后,通过请求的路径获得对应的GetDispatcher/PostDispatcher来调用对应的方法,然后通过方法获得对应的模型和视图ModelAndView,然后交给模板引擎ViewEngine来处理,模板引擎将处理后的内容写入到对浏览器的应答。整个工程的结构类似下面,其中的framework就是Spring MVC框架,controller包下是我们需要编写的业务逻辑。
web - mvc
├── pom.xml
└── src
└── main
├── java
│ └── xsl
│ ├── Main.java
│ ├── bean
│ │ ├── SignInBean.java
│ │ └── User.java
│ ├── controller
│ │ ├── IndexController.java
│ │ └── UserController.java
│ └── framework
│ ├── DispatcherServlet.java
│ ├── FileServlet.java
│ ├── GetMapping.java
│ ├── ModelAndView.java
│ ├── PostMapping.java
│ └── ViewEngine.java
└── webapp
├── WEB - INF
│ ├── templates
│ │ ├── _base.html
│ │ ├── hello.html
│ │ ├── index.html
│ │ ├── profile.html
│ │ └── signin.html
│ └── web.xml
└── static
├── css
│ └── bootstrap.css
└── js
├── bootstrap.js
└── jquery.js
可以看到我们可以包含多个控制器,如下在每个控制器类中设置一个注解RequestMapping,可以通过解析请求的首个路径名称来区别使用哪个控制器:
@RequestMapping("user")
public class UserController {
......
};
@RequestMapping("student")
public class StudentController {
......
};
http://HostName/user/edit
http://HostName/student/edit
4、Spring MVC 不使用反射的方法
如果不使用反射的话,那么只能是在DispatcherServlet中定义一个处理类RequestHandle类型的对象,当请求到来后调用该对象的handlePost() / handleGet()。我们的UserController应该从RequestHandle继承,然后赋给DispatcherServlet中的RequestHandle类型的对象。在UserController中重写handlePost() / handleGet(),在handlePost() / handleGet()中选择调用UserController的哪个方法。
public class RequestHandle{
public ModelAndView handleGet(HttpServletRequest req, HttpServletResponse resp){}
public ModelAndView handlePost(HttpServletRequest req, HttpServletResponse resp){}
}
public class DispatcherServlet extends HttpServlet{
RequestHandle requestHandle;
void setHandle(RequestHandle requestHandle){
this.requestHandle = requestHandle;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
ModelAndView mv = requestHandle.handleGet(req, resp);
//将ModelAndView交给模板引擎处理
......
}
}
public class UserController extends RequestHandle{
@Override
public ModelAndView handleGet(HttpServletRequest req, HttpServletResponse resp){
ModelAndView mv = null;
String path = req.getRequestURI().substring(req.getContextPath().length());
if(path == "/signin"){
mv = signin();
}else if(path == "/signout"){
HttpSession session = req.getSession();
mv = signout(session);
}
......
return mv;
}
@Override
public ModelAndView handlePost(HttpServletRequest req, HttpServletResponse resp){
ModelAndView mv = null;
String path = req.getRequestURI().substring(req.getContextPath().length());
if(path == "/signin"){
SignInBean bean = getBeanFromRequestBody(req);
mv = doSignin(bean);
}
......
return mv;
}
public ModelAndView signin() {
...
}
public ModelAndView doSignin(SignInBean bean) {
...
}
public ModelAndView signout(HttpSession session) {
...
}
......
}
5、总结
使用JSP的话只需要向Tomcat提供一个html文件就可以展现网站内容,在html文件中可以嵌入java代码,并引入HttpServletRequest、HttpServletResponse等Servlet对象来进行相关操作。
使用Servlet是在Java代码中添加HTML(HTTP请求处理中生成HTML内容),使用JSP是在HTML中添加Java代码(HTML文件中通过引入Servlet对象来获得展示数据)。可以使用MVC模式来处理Web请求,使Java代码和HTML不再混合,即页面展示和Web请求处理分离,在Web请求处理(Control)中生成页面展示需要的数据(Model)后传给页面(View)。
Spring MVC就使用了MVC模式,Servlet根据用户的请求来调用Control类中对应的方法,我们提供Control实现类来编写处理用户请求的方法,Control类中方法将需要展示的JSP页面文件即View和其需要的数据Model 一同交给模板引擎,由模板引擎将页面和数据组合为标准HTML内容后发送给浏览器。Spring MVC实际上并不是使用JSP作为页面文件,比如可以使用Pebble模板文件,其也是一个类似HTML的文件,同JSP一样,在模板文件中也可以引用传给它的Model数据,模板文件由对应的模板引擎来生成标准HTML内容。