Struts扫盲
这里的struts是struts1。以本文记录我的那些复习JavaEE的痛苦并快乐的晚上
Struts是什么
框架的概念想必大家都清楚,框架即“半成品代码”,是为了简化开发而设计的。一个项目有许多分层,拿一个MVC架构的Web应用来说,有表示层、控制层、业务逻辑层、数据访问层 多层(这是一种分法),用于各层的框架就可以被冠以对应的前缀。
在讲Struts之前,可以先了解一下Spring框架,这是一个开源的全面开发框架,见下图。可以看到Spring包含了相当多的模块。在Web模块中可以看到Struts的身影,没错,Struts就是一个Web层的框架,用来帮助我们完成Servlet的编码。
Web层通常指的是应用程序中负责处理用户请求、展示数据以及与用户交互的部分。按照上面的分法,Web层功能集中于表示层和控制层,不涉及数据的访问与处理。
Struts开发基本原理
以往基本的Web应用开发关注的都是Servlet该怎么写、该怎么处理请求。具体模型(Bean)调用的语句和请求处理的相关语句都在Servlet中。
而Struts帮我们简化了这个过程,我们不再编写Servlet代码,而是转而编写我们自己的Action
类,Action
类可以有很多个,分别实现不同的业务逻辑,负责处理请求,最好以功能来命名前缀,比如LoginAction
。
Struts1提供了一个ActionServlet
类来负责请求的调度(对请求进行预处理,并转发给对应的Action
类),而不负责具体请求的处理,起到的是(中央)控制器的功能。
另外还有ActionForm、ActionForward、ActionMapping
等组件,分别负责了之前Servlet的一小部分逻辑,这样的设计思路层次分明,各部分之前耦合度低,也易于修改。
Struts工作流程
-
用户发送一个请求到
ActionServlet
。 -
ActionServlet
根据请求URL查找struts-config.xml
中的相应Action
类。 -
Action
类的execute
方法处理请求逻辑,并返回一个ActionForward
,确定哪个视图(JSP页面)用于显示响应。 -
请求被转发到相应的JSP页面,JSP页面通过标签库(如Struts标签库)来展示数据。
url请求(.do结尾的)
—>ServletMapping截获请求,交给ActionServlet处理
—>struts-config.xml根据url指派具体的Action(相当于Controller层)
—>Action的execute()方法执行具体的业务逻辑(Controller–>Model层)
—>返回ActionForward对象,struts-config.xml分发具体转向的jsp页面(Controller–>View层)原文链接:https://blog.csdn.net/u014256815/article/details/80766990
Struts核心组件
为了更好的理解Struts的核心组件,让我们一起来完成一个基于Struts1框架的猜数游戏,并在这个过程中学习相关的知识点。
猜数游戏
原先的猜数游戏主要代码如下:
NumberGuessBean
package com.niko.bean;
import java.io.Serializable;
import java.util.Random;
public class NumberGuessBean implements Serializable {
private static final long serialVersionUID = 1L;
private int answer;
private String hint;
private int numGuesses;
private boolean success;
private final Random random = new Random();
public NumberGuessBean() {
reset();
}
public int getAnswer() {
return answer;
}
public void setAnswer(int answer) {
this.answer = answer;
}
public String getHint() {
return "" + hint;
}
public void setHint(String hint) {
this.hint = hint;
}
public void setNumGuesses(int numGuesses) {
this.numGuesses = numGuesses;
}
public int getNumGuesses() {
return numGuesses;
}
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public void setGuess(String guess) {
numGuesses++;
int g;
try {
g = Integer.parseInt(guess);
} catch (NumberFormatException e) {
g = -1;
}
if (g == answer) {
success = true;
} else if (g == -1) {
hint = "a number next time";
} else if (g < answer) {
hint = "higher";
} else if (g > answer) {
hint = "lower";
}
}
public void reset() {
answer = Math.abs(random.nextInt() % 100) + 1;
success = false;
numGuesses = 0;
}
}
numguess.jsp
<%@ page import = "com.niko.bean.NumberGuessBean" %>
<jsp:useBean id="numguess" class="com.niko.bean.NumberGuessBean" scope="session"/>
<jsp:setProperty name="numguess" property="*"/>
<html>
<head><title>Number Guess</title></head>
<body bgcolor="white">
<font size=4>
<% if (numguess.getSuccess()) { %>
Congratulations! You got it.
And after just <%= numguess.getNumGuesses() %> tries.<p>
<% numguess.reset(); %>
Care to <a href="numguess.jsp">try again</a>?
<% } else if (numguess.getNumGuesses() == 0) { %>
Welcome to the Number Guess game.<p>
I'm thinking of a number between 1 and 100.<p>
<form method=get>
What's your guess? <input type=text name=guess>
<input type=submit value="Submit">
</form>
<% } else { %>
Good guess, but nope. Try <b><%= numguess.getHint() %></b>.
You have made <%= numguess.getNumGuesses() %> guesses.<p>
I'm thinking of a number between 1 and 100.<p>
<form method=get>
What's your guess? <input type=text name=guess>
<input type=submit value="Submit">
</form>
<% } %>
</font>
</body>
</html>
这样虽然可以实现功能,但是显示逻辑和具体业务逻辑混杂在一起了,不适合当我们接下来Struts应用的框架。
改进版猜数游戏
为了分离显示逻辑和业务逻辑,根据猜数的流程设计了三个jsp
界面,并且在servlet中使用request.getRequestDispatcher("welcome.jsp").forward(req, resp);
的方式来跳转的JSP页面。
getRequestDispatcher()
是ServletRequest
接口中的方法,用于获取RequestDispatcher
对象。
RequestDispatcher
对象的forward()
方法会将请求从一个Servlet转发到另一个Servlet或JSP页面,以便处理或显示响应数据。
NumberGuessBean
同上
NumberGuessServlet
package com.niko.controller;
import com.niko.bean.NumberGuessBean;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Random;
public class NumberGuessServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
req.setCharacterEncoding("utf-8");
//创建Bean对象(错误),这个方法是每次请求都会被调用的,这样的创建方式会导致Bean的状态不会被保存,
//每次请求都new了一个新对象。
/*NumberGuessBean numberGuessBean = new NumberGuessBean();
String guess=req.getParameter("guess");
if(guess!=null){
numberGuessBean.setGuess(guess);
}
*/
//创建Bean对象(正确),使用session,这样可以在多次猜测尝试过程中保存状态数据
HttpSession session = req.getSession();
NumberGuessBean numberGuessBean = (NumberGuessBean) session.getAttribute("numberGuessBean");
if (numberGuessBean == null) {
numberGuessBean = new NumberGuessBean();
session.setAttribute("numberGuessBean", numberGuessBean);
}
String guess = req.getParameter("guess");
//去空格
if (guess==null || guess.length()==0) {guess="";}
guess=guess.trim();
if(guess!=null){
numberGuessBean.setGuess(guess);
}
if(numberGuessBean.getSuccess()){
req.getRequestDispatcher("success.jsp").forward(req, resp);
numberGuessBean.reset();
session.setAttribute("numberGuessBean", numberGuessBean);
} else if (numberGuessBean.getNumGuesses()==0) {
req.getRequestDispatcher("welcome.jsp").forward(req, resp);
} else {
req.getRequestDispatcher("continue.jsp").forward(req, resp);
session.setAttribute("numberGuessBean", numberGuessBean);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
continue.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>number guess game</title>
</head>
<body>
Good guess, but nope. Try <b> ${sessionScope.numberGuessBean.hint} </b>.
You have made ${sessionScope.numberGuessBean.numGuesses} guesses.<p>
I'm thinking of a number between 1 and 100.<p>
<form method="get" action="/NumGuess/numguess">
What's your guess? <input type=text name=guess>
<input type=submit value="Submit">
</form>
</body>
</html>
success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>number guess game</title>
</head>
<body>
Congratulations! You got it.
And after just ${sessionScope.numGuessBean.numGuesses} tries.<p>
Care to <a href="/NumGuess/welcome.jsp">try again</a>?
</body>
</html>
welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>number guess game</title>
</head>
<body>
Welcome to the Number Guess game.<p>
I'm thinking of a number between 1 and 100.<p>
<form method="get" action="/NumGuess/numguess">
What's your guess? <input type=text name=guess>
<input type=submit value="Submit">
</form>
</body>
</html>
好了,在上面的猜数游戏基础上,来进行基于Struts
的改造。
ActionServlet
- 功能:控制请求的调度,转发请求到相应的Action类。
Action
- 功能:执行业务逻辑,处理请求,返回结果。
首先肯定需要砍掉原来的Servlet,转而用Action
来代替,我们想要设计一个Action类,来接受传入的猜测数字,并根据猜测结果将请求转发到对应的jsp
文件来显示。
设计一个NumGuessAction
,继承自Action
package com.niko.action;
import com.niko.bean.NumberGuessBean;
import org.apache.struts.action.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class NumGuessAction extends Action {
@Override
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取表单数据:并将其转换为DynaActionForm类型,方便动态获取和设置表单参数
DynaActionForm daForm = (DynaActionForm) form;
String guess = request.getParameter("guess");// 获取用户输入的猜测数字(从表单中取值)
// 获取会话对象,如果会话不存在则创建一个新的会话.从会话中获取当前的NumberGuessBean对象(用于存储游戏数据)
HttpSession session = request.getSession();//session中存的是多次请求需要用到的持久对象,比如这里的Bean
NumberGuessBean numberGuessBean=(NumberGuessBean)session.getAttribute("numberGuessBean");
//创建Bean对象:如果这是第一次请求,Bean还没有创建,则new一个,并且我们需要在多次请求中使用这个Bean,因此要存到session中去(用setter来设置)
if (numberGuessBean==null){
numberGuessBean=new NumberGuessBean();
session.setAttribute("numberGuessBean",numberGuessBean);
}
//开始处理转发逻辑:1.把本次猜的数传给Bean
numberGuessBean.setGuess(guess);
//处理转发逻辑:2.利用Bean的方法来判断是否猜测成功,并转发到对应的界面
//有一个改进的想法,这部分其实也算是猜数逻辑(业务),可以把是否猜成功的逻辑给放到Bean里头,有兴趣的可以试一试
if(numberGuessBean.getSuccess()){
//表示猜数成功。至于为什么要set表单数据,先按下不表,设计后面JSP读取数据逻辑的更改(因为开始使用ActionForm)
daForm.set("NumGuesses",numberGuessBean.getNumGuesses());
numberGuessBean.reset();//重启游戏以便重玩
//!!这里也很重要,用到了四个形参之一mapping,这个forward方法类似request的getRequestDispatcher的forward方法,负责的是页面跳转
//!!另外括号里的叫做逻辑名称,不再是JSP文件名字,逻辑名称和JSP文件之间的映射关系在strusts-config.xml中配置
return mapping.findForward("success");
}else if(numberGuessBean.getNumGuesses()==0){
//表示第一次猜数。转到欢迎视图
return mapping.findForward("welcome");
}else{
//表示继续猜数。转到继续视图,相比成功猜数,需要多存一个hint
daForm.set("NumGuesses",numberGuessBean.getNumGuesses());
daForm.set("Hint",numberGuessBean.getHint());
return mapping.findForward("continue");
}
}
}
ActionForm
- ActionForm:封装表单数据。
可以看到在上面的NumGuessAction
的实现中使用到了ActionForm
(实际用到的是继承版本的DynaActionForm
)。
也许有人会问actionForm这个东西怎么这么多余呢,之前的猜数游戏是在JSP页面里直接用类似
Congratulations! You got it. And after just <%= numGuessBean.getNumGuesses() %> tries.<p>
这样的语句直接读取bean里面的数据,
现在使用form表单反而需要先存入form,再在jsp文件里读取property属性:
Congratulations! You got it. And after just <bean:write name="guessBean" property="numGuesses" /> tries.
这样做到底有什么意义?
这也是我的心路历程,现在可以回答这个问题,通过略繁琐的步骤:将用户输入的数据(表单数据)从前端提交到后端进行处理→通过 Action 类封装成一个 JavaBean→再转交给 JSP 页面。 完成了显示逻辑和业务逻辑之间的解耦,实际充当视图的JSP文件不应当知晓内部类有什么方法,而是直接和表单(实际也是一个JavaBean)进行交互。为什么要解耦呢,举一个例子来说,如果猜数Bean的逻辑有所更改,那么所有的JSP文件都要修改,这显然是我们不希望看到的。
ActionForward
- ActionForward:用于指定请求处理完成后,跳转到哪个视图(JSP)。
ActionMapping
- ActionMapping: 主要用于将用户请求映射到相应的
Action
类。
在上面的NumGuessAction
类中最后的转发逻辑使用到了这两个类,首先NumGuessAction
类的execute
方法的返回值就是ActionForward
对象(有name
和path
属性),显然mapping.findForward
的返回值也是ActionForward
对象,findForward
需要传入一个转发页面的逻辑名称(在struts-config.xml中配置映射关系)
还有一个返回值为String
数组的findForwards
方法用于返回当前模块的所有ActionForward
名称,详见API。
struts-config.xml
- struts-config.xml:配置文件,定义请求和响应之间的映射关系。
默认情况下,struts-config.xml
的名称是固定的,通常就是 struts-config.xml
。它一般位于项目的 WEB-INF
目录下,即 WEB-INF/struts-config.xml
。这是 Struts 框架默认会加载的配置文件路径。
好了!现在让我们来配置这个xml文件。首先是文件的一些常规的版本声明信息。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.3//EN"
"http://struts.apache.org/dtds/struts-config_1_3.dtd">
接着使用标签<struts-config>
,其中有两个元素<form-beans>
<action-mappings>
<struts-config>
<form-beans>
<form-bean name="" type="">
<!--具体看下面-->
</form-bean>
</form-beans>
<action-mappings>
<action path="" type="" name="">
<!--具体看下面-->
</action>
</action-mappings>
</struts-config>
先看<form-beans>
,之前讲到ActionForm
最终会被转化成JavaBean,那这里就是来设置JavaBean的属性的,下面的语句块
name
属性指定的在JSP中访问的form名称;
type
指定的是使用DynaActionForm
的全类名;
<form-bean name="guessBean" type="org.apache.struts.action.DynaActionForm">
<form-property name="guess" type="java.lang.String" />
<form-property name="hint" type="java.lang.String" />
<form-property name="numGuesses" type="java.lang.String"/>
</form-bean>
可以暂且理解作(当然实现是截然不同的)
Class guessBean{
String guess;
String hint;
String numGuesses
}
接着看<action-mappings>
标签,
path
和type
指定的是表示请求的 URL 路径,也就是说当URL中是/guess时,就会把请求交给后面type指定的Action
类来处理;
name
属性指定的是要和这个Action
类所绑定的ActionForm
类的名字,一般在上面的<form-bean>
标签那里就声明了。
<action-mappings>
<action path="/guess" type="com.niko.struts.NumGuessAction" name="guessBean">
<forward name="welcome" path="/jsp/welcome.jsp" />
<forward name="continue" path="/jsp/continue.jsp" />
<forward name="success" path="/jsp/success.jsp" />
</action>
</action-mappings>
JSP界面
最后附上三个改动的jsp文件
welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Number Guess game</title>
</head>
<body>
Welcome to the Number Guess game.
<p>I'm thinking of a number between 1 and 100.</p>
<p>
<html:form action="/guess" method="get">
What's your guess?
<html:text property="guess"/>
<html:submit value="Submit"/>
</html:form>
</p>
</body>
</html>
continue.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Number Guess game</title>
</head>
<body> <!--可以看到使用自定义标签获取键值对并输出的方法-->
Good guess, but nope. Try <b> <bean:write name="guessBean" property="hint" /></b>.
You have made <bean:write name="guessBean" property="numGuesses" />
guesses.
<p>I'm thinking of a number between 1 and 100.
<p>
<html:form action="/guess" method="get">
What's your guess? <html:text property="guess" />
<html:submit value="Submit" />
</html:form>
</body>
</html>
success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Number Guess game</title>
</head>
<body>
Congratulations! You got it. And after just
<bean:write name="guessBean" property="numGuesses" />
tries.
<p>
Care to <html:link page="/guess">try again?</html:link>
</body>
</html>
接下来会阅读一下和负责转发的控制器有关的源码,搞清struts是怎么简化Action类的请求分发的。
html>
#### success.jsp
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Number Guess game</title>
</head>
<body>
Congratulations! You got it. And after just
<bean:write name="guessBean" property="numGuesses" />
tries.
<p>
Care to <html:link page="/guess">try again?</html:link>
</body>
</html>
接下来会阅读一下和负责转发的控制器有关的源码,搞清struts是怎么简化Action类的请求分发的。