2.3 第三阶段
在第二阶段中,基本的功能已经实现了。在此基础上进行优化,引入MVC 。将前面四个servlet整合在一起,放在一个FruitServlet
中,如下:
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310101924425.png" alt="image-20231010192433274" style="zoom: 50%;" />
1、合并servlet至FruitServlet
@WebServlet("/fruit.do")
public class FruitServlet extends ViewBaseServlet {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
// 通过request获取操作符的值
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)) {
operate = "index";
}
// 通过操作符来响应对应的操作
switch (operate) {
case "index":
index(request,response);
break;
case "add":
add(request,response);
break;
case "del":
del(request, response);
break;
case "edit":
edit(request, response);
break;
case "update":
update(request, response);
default:
throw new RuntimeException("operate值非法!");
}
}
private void update(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1.设置编码
request.setCharacterEncoding("utf-8");
//2.获取参数
String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);
String fname = request.getParameter("fname");
String priceStr = request.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);
String remark = request.getParameter("remark");
//3.执行更新
fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
//4.资源跳转
//super.processTemplate("index",request,response);
//request.getRequestDispatcher("index.html").forward(request,response);
//此处需要重定向,目的是重新给IndexServlet发请求,重新获取furitList,然后覆盖到session中,这样index.html页面上显示的session中的数据才是最新的
response.sendRedirect("fruit.do");
}
private void edit(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
String fidStr = request.getParameter("fid");
if(StringUtil.isNotEmpty(fidStr)){
int fid = Integer.parseInt(fidStr);
Fruit fruit = fruitDAO.getFruitByFid(fid);
request.setAttribute("fruit",fruit);
super.processTemplate("edit",request,response);
}
}
private void del(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
String fidStr = request.getParameter("fid");
if(StringUtil.isNotEmpty(fidStr)){
int fid = Integer.parseInt(fidStr);
fruitDAO.delFruit(fid);
//super.processTemplate("index",request,response);
response.sendRedirect("fruit.do");
}
}
private void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String fname = request.getParameter("fname");
Integer price = Integer.parseInt(request.getParameter("price")) ;
Integer fcount = Integer.parseInt(request.getParameter("fcount"));
String remark = request.getParameter("remark");
Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
fruitDAO.addFruit(fruit);
response.sendRedirect("fruit.do");
}
private void index(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
HttpSession session = request.getSession() ;
// 设置当前页,默认值1
Integer pageNo = 1 ;
String oper = request.getParameter("oper");
//如果oper!=null 说明 通过表单的查询按钮点击过来的
//如果oper是空的,说明 不是通过表单的查询按钮点击过来的
String keyword = null ;
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
//说明是点击表单查询发送过来的请求
//此时,pageNo应该还原为1 , keyword应该从请求参数中获取
pageNo = 1 ;
keyword = request.getParameter("keyword");
//如果keyword为null,需要设置为空字符串"",否则查询时会拼接成 %null% , 我们期望的是 %%
if(StringUtil.isEmpty(keyword)){
keyword = "" ;
}
//将keyword保存(覆盖)到session中
session.setAttribute("keyword",keyword);
}else{
//说明此处不是点击表单查询发送过来的请求(比如点击下面的上一页下一页或者直接在地址栏输入网址)
//此时keyword应该从session作用域获取
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)){
pageNo = Integer.parseInt(pageNoStr); //如果从请求中读取到pageNo,则类型转换。否则,pageNo默认就是1
}
//如果不是点击的查询按钮,那么查询是基于session中保存的现有keyword进行查询
Object keywordObj = session.getAttribute("keyword");
if(keywordObj!=null){
keyword = (String)keywordObj ;
}else{
keyword = "" ;
}
}
// 重新更新当前页的值
session.setAttribute("pageNo",pageNo);
FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
session.setAttribute("fruitList",fruitList);
//总记录条数
int fruitCount = fruitDAO.getFruitCount(keyword);
//总页数
int pageCount = (fruitCount+5-1)/5 ;
/*
总记录条数 总页数
1 1
5 1
6 2
10 2
11 3
fruitCount (fruitCount+5-1)/5
*/
session.setAttribute("pageCount",pageCount);
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//所以真实的视图名称是: / index .html
super.processTemplate("index",request,response);
}
}
2、更新service方法
/*通过反射调用匹配的方法*/
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)) {
operate = "index";
}
// 1. 获取当前类中声明的所有方法。
Method[] methods = this.getClass().getDeclaredMethods();
// 2. 遍历所有方法。
for (Method m : methods) {
// 3. 获取当前遍历到的方法的名称。
String methodName = m.getName();
// 4. 判断当前遍历到的方法的名称是否与传入的操作名称(通过"operate"参数获取)相等。
if(operate.equals(methodName)) {
try {
// 5. 如果匹配成功,则尝试调用该方法,并传入请求和响应对象。
m.invoke(this, request, response);
// 6. 如果调用成功,则返回。
return ;
} catch (IllegalAccessException e) {
// 7. 如果没有找到匹配的方法,则抛出异常。
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
3、引入前端控制器DispatcherServlet
和controller层
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310110956569.png" alt="image-20231011095632494" style="zoom:50%;" />
它接收所有的请求并将其分发给合适的处理器(Controller),同时新建包controller
,将FruitServlet
改为FruitController
放入该包中。
DispatcherServlet
作用:
1.根据url定位到能够处理这个请求的controller组件: 1)从url中提取servletPath : /fruit.do -> fruit 2)根据fruit找到对应的组件:FruitController , 这个对应的依据我们存储在applicationContext.xml中
<bean id="fruit" class="com.lowell.fruit.controllers.FruitController/>
通过DOM技术我们去解析XML文件,在中央控制器中形成一个beanMap容器,用来存放所有的Controller组件 3)根据获取到的operate的值定位到我们FruitController中需要调用的方法
2.调用Controller组件中的方法:
-
获取参数 获取即将要调用的方法的参数签名信息: Parameter[] parameters = method.getParameters(); 通过parameter.getName()获取参数的名称; 准备了Object[] parameterValues 这个数组用来存放对应参数的参数值 另外,我们需要考虑参数的类型问题,需要做类型转化的工作。通过parameter.getType()获取参数的类型
-
执行方法
Object returnObj = method.invoke(controllerBean , parameterValues);
-
视图处理 String returnStr = (String)returnObj; if(returnStr.startWith("redirect:")){ .... }else if.....
在application.xml
中配置DispatcherServlet
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<bean id="fruit" class="com.lowell.fruit.controller.FruitController"/>
</beans>
DispatcherServlet
代码
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{
private Map<String,Object> beanMap = new HashMap<>();
public DispatcherServlet(){
}
@Override
public void init(){
try {
/* Java的DOM方式解析XML文件 */
// 获取XML文件的输入流
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class controllerBeanClass = Class.forName(className);
Object beanObj = controllerBeanClass.newInstance() ;
Method setServletContextMethod = controllerBeanClass.getDeclaredMethod("setServletContext", ServletContext.class);
setServletContextMethod.invoke(beanObj , this.getServletContext());
beanMap.put(beanId , beanObj) ;
}
}
} catch (ParserConfigurationException | SAXException | IOException | IllegalAccessException |
InstantiationException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置编码
request.setCharacterEncoding("UTF-8");
//假设url是: http://localhost:8080/pro15/hello.do
//那么servletPath是: /hello.do
// 我的思路是:
// 第1步: /hello.do -> hello 或者 /fruit.do -> fruit
// 第2步: hello -> HelloController 或者 fruit -> FruitController
String servletPath = request.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do") ;
servletPath = servletPath.substring(0,lastDotIndex);
Object controllerBeanObj = beanMap.get(servletPath);
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
try {
Method method = controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class,HttpServletResponse.class);
if(method!=null){
method.setAccessible(true);
method.invoke(controllerBeanObj,request,response);
}else{
throw new RuntimeException("operate值非法!");
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
上述代码解释如下:
init()
- 通过
getClass().getClassLoader().getResourceAsStream("applicationContext.xml")
从类路径中获取XML文件的输入流 - 创建
DocumentBuilderFactory
实例 - 通过
documentBuilderFactory.newDocumentBuilder()
创建DocumentBuilder
对象。 - 通过
documentBuilder.parse(inputStream)
方法将XML文件的输入流解析成Document
对象 - 从解析的XML文件中获取所有的
bean
节点,并将每个bean
节点的id
和对应的class
属性值存储在beanMap
中- 遍历
beanNodeList
,即所有的bean
节点 - 对于每个
bean
节点,首先判断其节点类型是否为元素节点 - 获取当前
bean
节点的id
和class
属性值 - 通过
Class.forName(className)
加载className
对应的类,并将其实例化得到beanObj
- 通过反射获取
controllerBeanClass
类中声明的名为setServletContext
、参数为ServletContext
类型的方法,并调用该方法设置ServletContext
- 将
beanId
和beanObj
存储在beanMap
中
- 遍历
service()
- 设置请求的字符编码为UTF-8,保证正确地解析请求参数的中文字符
- 获取请求的Servlet路径,并去掉开头的斜杠
- 查找Servlet路径中最后一个".do"的位置,并截取到该位置之前的字符串,作为操作的标识(
servletPath
) - 根据
beanMap
中通过操作标识查找到对应的控制器实例对象(controllerBeanObj
) - 获取请求参数
operate
的值,若为空,则设置默认值为index
- 根据操作标识调用对应的控制器方法进行处理
- 利用反射,通过控制器实例对象的
getClass()
方法获取到实际类的Class
对象 - 利用
getDeclaredMethod(operate, HttpServletRequest.class, HttpServletResponse.class)
方法获取到指定名称和参数类型的方法对象(method
) - 如果找到了该方法,则设置该方法可以访问(
setAccessible(true)
),并通过invoke(controllerBeanObj, request, response)
方法调用该方法 - 如果没有找到指定的方法,则抛出运行时异常并输出错误信息。
- 捕获
NoSuchMethodException
、IllegalAccessException
和InvocationTargetException
异常,并打印异常堆栈信息
- 利用反射,通过控制器实例对象的
4、引入IOC容器和service层
实现尽可能高内聚低耦合 层内部的组成应该是高度聚合的,而层与层之间的关系应该是低耦合的,最理想的情况0耦合(就是没有耦合)
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310111947958.png" alt="image-20231011194713796" style="zoom:50%;" />
BeanFactory
package com.atguigu.myssm.io;
public interface BeanFactory {
Object getBean(String id);
}
ClassPathXmlApplicationContext
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map<String,Object> beanMap = new HashMap<>();
public ClassPathXmlApplicationContext(){
try {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class beanClass = Class.forName(className);
//创建bean实例
Object beanObj = beanClass.newInstance() ;
//将bean实例对象保存到map容器中
beanMap.put(beanId , beanObj) ;
//到目前为止,此处需要注意的是,bean和bean之间的依赖关系还没有设置
}
}
//5.组装bean之间的依赖关系
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE) {
Element beanElement = (Element) beanNode;
String beanId = beanElement.getAttribute("id");
NodeList beanChildNodeList = beanElement.getChildNodes();
for (int j = 0; j < beanChildNodeList.getLength() ; j++) {
Node beanChildNode = beanChildNodeList.item(j);
if(beanChildNode.getNodeType()==Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
Element propertyElement = (Element) beanChildNode;
String propertyName = propertyElement.getAttribute("name");
String propertyRef = propertyElement.getAttribute("ref");
//1) 找到propertyRef对应的实例
Object refObj = beanMap.get(propertyRef);
//2) 将refObj设置到当前bean对应的实例的property属性上去
Object beanObj = beanMap.get(beanId);
Class beanClazz = beanObj.getClass();
Field propertyField = beanClazz.getDeclaredField(propertyName);
propertyField.setAccessible(true);
propertyField.set(beanObj,refObj);
}
}
}
}
} catch (ParserConfigurationException | SAXException | IOException | IllegalAccessException |
InstantiationException | ClassNotFoundException | NoSuchFieldException e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String id) {
return beanMap.get(id);
}
}
applicationContext.xml
<?xml version="1.0" encoding="utf-8"?>
<beans>
<bean id="fruitDAO" class="com.atguigu.fruit.dao.impl.FruitDAOImpl"/>
<bean id="fruitService" class="com.atguigu.fruit.service.impl.FruitServiceImpl">
<!-- property标签用来表示属性;name表示属性名;ref表示引用其他bean的id值-->
<property name="fruitDAO" ref="fruitDAO"/>
</bean>
<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
</beans>
DispatcherServlet
中init()
方法
public void init() throws ServletException {
super.init();
beanFactory = new ClassPathXmlApplicationContext();
}
service层中FruitService
和FruitServiceImpl
public interface FruitService {
//获取指定页面的库存列表信息
List<Fruit> getFruitList(String keyword , Integer pageNo);
//添加库存记录信息
void addFruit(Fruit fruit);
//根据id查看指定库存记录
Fruit getFruitByFid(Integer fid);
//删除特定库存记录
void delFruit(Integer fid);
//获取总页数
Integer getPageCount(String keyword);
//修改特定库存记录
void updateFruit(Fruit fruit);
}
public class FruitServiceImpl implements FruitService {
private FruitDAO fruitDAO = null ;
@Override
public List<Fruit> getFruitList(String keyword, Integer pageNo) {
return fruitDAO.getFruitList(keyword,pageNo);
}
@Override
public void addFruit(Fruit fruit) {
fruitDAO.addFruit(fruit);
}
@Override
public Fruit getFruitByFid(Integer fid) {
return fruitDAO.getFruitByFid(fid);
}
@Override
public void delFruit(Integer fid) {
fruitDAO.delFruit(fid);
}
@Override
public Integer getPageCount(String keyword) {
int count = fruitDAO.getFruitCount(keyword);
int pageCount = (count+5-1)/5 ;
return pageCount;
}
@Override
public void updateFruit(Fruit fruit) {
fruitDAO.updateFruit(fruit);
}
}
第三阶段结果
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310111959438.png" alt="image-20231011195918341" style="zoom:50%;" />
2.4 第四阶段
为了确保数据库操作的一致性和完整性,引入事务管理,引入监听器和过滤器
TransactionManager
package com.lowell.myssm.trans;
import com.lowell.myssm.basedao.ConnUtil;
import java.sql.Connection;
import java.sql.SQLException;
public class TransactionManager {
public static void beginTrans() throws SQLException {
ConnUtil.getConn().setAutoCommit(false);
}
public static void commit() throws SQLException {
Connection conn = ConnUtil.getConn();
conn.commit();
ConnUtil.closeConn();
}
public static void rollback() throws SQLException {
Connection conn = ConnUtil.getConn();
conn.rollback();
ConnUtil.closeConn();
}
}
ConnUtil
package com.lowell.myssm.basedao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnUtil {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static final String DRIVER = "com.mysql.jdbc.Driver" ;
public static final String URL = "jdbc:mysql://localhost:3306/fruitdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
public static final String USER = "root";
public static final String PWD = "******";
public static Connection createConn() {
try {
//1.加载驱动
Class.forName(DRIVER);
//2.通过驱动管理器获取连接对象
return DriverManager.getConnection(URL, USER, PWD);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null;
}
public static Connection getConn() {
Connection conn = threadLocal.get();
if(conn == null) {
conn = createConn();
threadLocal.set(conn);
}
return threadLocal.get();
}
public static void closeConn() throws SQLException {
Connection conn = threadLocal.get();
if(conn == null) {
return ;
}
if(!conn.isClosed()) {
conn.close();
threadLocal.set(null);
}
}
}
CharecterEncodingFilter
package com.lowell.myssm.filters;
@WebFilter(urlPatterns = {"*.do"}, initParams = {@WebInitParam(name= "encoding", value = "GBK")})
public class CharecterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String encodingStr = filterConfig.getInitParameter("encoding");
if(StringUtil.isNotEmpty(encodingStr)) {
encoding = encodingStr;
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
((HttpServletRequest)servletRequest).setCharacterEncoding("UTF-8");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
OpenSessionInViewFilter
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try{
TransactionManager.beginTrans();
System.out.println("开启事务...");
filterChain.doFilter(servletRequest, servletResponse);
TransactionManager.commit();
System.out.println("提交事务...");
} catch(Exception e) {
e.printStackTrace();
try {
TransactionManager.rollback();
System.out.println("回滚事务...");
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}
@Override
public void destroy() {
}
}
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310112034239.png" alt="image-20231011203423154" style="zoom:50%;" />
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310112034042.png" alt="image-20231011203454960" style="zoom:50%;" />
至此,水果后台管理系统的功能,均已实现。
源码:[github-Lowell-37](