# Java Web教学项目 **Repository Path**: yuyaoting/javaweb_teaching_project ## Basic Information - **Project Name**: Java Web教学项目 - **Description**: 该项目是基于MVC的Java Web基础教学项目,不涉及前端内容。 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 56 - **Created**: 2018-09-14 - **Last Updated**: 2022-05-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Java Web教学项目 #——基于MVC的博客网站设计与实现 该项目是基于MVC的Java Web基础教学项目,不涉及前端内容。相关的前端内容请访问同账号的其他项目。 该项目为至少要运行在JDK1.8环境中,建议运行在JDK 9或JDK 10中;服务器建议使用tomcat 9或Jetty 12版本。 ## 软件架构 最基础的MVC模式:JSP+Servlet+JavaBean结构 使用JSTL/EL、原生AJAX等技术。 ## 前置任务列表 完成该项目需要一些首先完成一些前置任务。在掌握前置任务之后,才能完成该项目 ### 1,构建基于maven的项目 构建基于maven的web项目,第三方jar包依赖于中央仓库。 配置maven\conf\settiong.xml文件 修改本地仓库的地址;新增镜像仓库地址 ``` E:\yuhf\.m2\repository ``` ```xml alimaven central aliyun maven http://maven.aliyun.com/nexus/content/repositories/central/ ``` 配置pom.xml文件 ```xml 4.0.0 edu.yuhf blogServer 0.0.1-SNAPSHOT org.apache.maven.plugins maven-war-plugin 3.1.0 false org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 org.apache.maven.plugins maven-war-plugin src/main/webapp true WEB-INF org.eclipse.jetty jetty-maven-plugin 9.4.5.v20170502 10 9000 / src/test/resources/webdefault.xml javax.servlet javax.servlet-api 3.1.0 provided jstl jstl 1.2 javax.servlet.jsp javax.servlet.jsp-api 2.2.1 provided junit junit 4.12 test log4j log4j 1.2.17 commons-fileupload commons-fileupload 1.2.2 com.oracle ojdbc6 12.1.0 com.alibaba fastjson 1.2.49 war ``` ### 2,利用Git控制项目版本 学会使用Git软件的用法,并确定远程仓库。本文建议使用码云。 ## 项目细分任务列表 ### 1,登录流程 创建不带数据库验证的用户登录流程。熟悉web开发中的基于MVC设计模式的开发流程。 注意,登录流程实际上包含两个相互独立的流程。 index.jsp-->init.servlet-->login.jsp 流程1 login.jsp-->user.servlet-->login.jsp或veiws/main.jsp 流程2 ### 2,登录流程附加功能 在用户登录流程中,加入session验证、cookie校验功能。 ### 3,设计数据库 使用PowerDesigner软件设计数据库的物理模型。基本参照如下: 1,用户信息管理模块:用户表、用户详情表、行政区划表、爱好表 2,文章信息管理模块:文章分类表、文章信息表 3,用户权限管理模:角色表、权限表、用户角色表、角色权限表 要求完成基本的设计工作,生成pdm文件。 ### 4,表空间、表的创建工作 完成数据库的创建,创建nnblog表空间,创建nnblog用户并授权,创建users表,并添加部分数据。 以上创建工作全部手写完成,源代码文件放入项目文件中。 ### 5,数据访问工具类 创建如下:java文件,完成数据库访问代码封装和优化,实现数据库连接信息软编码。 完成对一般的数据库操作(CRUD)的封装。可以返回实体类实例。 DBConnection.java ```java static{} public Connection getConnection(); public void closeConnection(Connection connection); ``` JdbcTemplate.java ```java public List query(String sql,ResultSetHandle rsh,Object...param); public int queryForCount(String sql,Object...param); public int update(String sql,Object...param); ``` ResultSetHandler.java --接口 TransactionManager.java jdbc.properties 要求:引入log4j日志系统 ### 6,登录流程_增强 加入service(业务部分)、dao(数据访问部分)及任务4中的数据库访问工具类。 service、dao都必须基于接口、且进行专门的单元测试。 ```java UserService UserServiceImpl UserDao UserJdbcDaoImpl UserServiceImplTest UserJdbcDaoImplTest ``` ### 7,数据库表的创建工作 创建相应的数据库表,包括:用户详情表、爱好信息表、行政区划表 ### 8,完成注册任务流程 注册任务细化: 1,编写用户注册页面,主要注册信息包括:用户名、密码、性别、爱好、来自于和Email。 2,在登录页面点击注册按钮,通过user.servlet?param=register转向注册页面。 3,在servlet中收集所需要的信息,包括:爱好信息,来自于的省级信息等。 4,在register.jsp页面中显示相关信息。 5,在省级下拉列表变化时,市级下拉列表显示相对应的信息。(利用ajax完成) 6,提交form表单到服务器端 7,服务端根据提交信息写入数据库,完成用户注册。 ### 9,创建文章相关的数据库表 文章信息表、文章分类表。 ### 10,构建基本的主页页面。 基于Grid,构建主页页面 ### 11,用户信息管理页面(关键页面) 完成用户信息管理页面的开发任务 #### 管理页面的任务细化 1,构建基础页面,显示用户信息。用户信息从users表、userDetail表及NativePlace表中获取。还要在Hobby表通过业务代码进行相应的转换处理。 利用List>结构进行多表联合查询结果存储工作。 利用专门的业务代码替换原结构中的的编码为更友好的中文显示。 2,对表格进行适度的美化处理,增加删、改、查功能;增分页按钮功能。 4,完成删、改、查操作,每个一个操作都是一个完整的处理流程。注意改操作是两个流程。 5,完成分页功能的编写工作。 6,和主页连接。 #### 提高任务 1,行政区划未选择时,显示列表显示红色的“未知”二字。 2,在显示列表中,行政区划一栏,改为省分,城市的写法。如:原来为“济南”,改为:“山东济南”。 3,考虑到用户起名方便,在数据库中加入昵称字段,用户名为英文、昵称字段可以录入中文信息。 #### 高难度任务解析 #### 1,显示爱好信息 在修改页中,每次自动显示爱好信息,并在已经选择的信息上打对号。 思路:在Hobby类中,加入一个表中没有的属性:private boolean selected,用以说明某个用户的选择。之后将Hobby表中的数据和用户数据中的hobby_code数据迭加,为用户选择了的数据,添加selected为真。显示时,把为真的数据显示为checked,就可以了。 #### 2,区域显示 修改页面中显示用户来自的区域,同时将用户已经选择的区域作为选中区域显示。 思路:与爱好类似,在NativePlace类中加入表没有的属性,以存储选择状态。然后在获得地区数据时,对数据进行比对。 具体的: ```java @Override public List setProvinceSelected(String code) { List list=nativePlaceDao.getLevel1(); String provinceCode=code.substring(0,2); list.stream().filter((item)-> Objects.equals(item.getCode(),provinceCode)).forEach((item)->item.setSelected(true)); return list; } @Override public List setCitySelected(String code) { List list=nativePlaceDao.getLevel2(code.substring(0, 2)); list.stream().filter((item)-> Objects.equals(item.getCode(),code)).forEach((item)->item.setSelected(true)); return list; } ``` #### 3,分页流程的编码步骤 1,分页准备,了解Oracle数据库分页代码的原理,掌握Oracle数据库为页的编码方式。 2,创建Page类,封装分页相关数据。 3,创建分页方法,一般原型为:`queryByPage(Page page);` 4,初始页面分页 5,上、下页换功能(重要难点在前端) 6,跳页功能的实现 #### 4,在分页的基础上实现即席查询 难点主要有两个: 实现多字段查询,这需要灵活的操作数据库,并根据查询字段的组合进行sql语句的拼装。大致的代码如下: ```java public void queryByPage(Page>> page, UMQueryKeyword qk) { List params=new ArrayList<>(); if(!Objects.equals("",qk.getName())) { params.add("%"+qk.getName()+"%"); } if(!Objects.equals("",qk.getSex())) { params.add(qk.getSex()); } if(!Objects.equals("",qk.getHobbyCode())) { params.add("%"+qk.getHobbyCode()+"%"); } params.add(page.getCurrentPage()); params.add(page.getRowNumber()); params.add(page.getCurrentPage()); params.add(page.getRowNumber()); String beginSQL="select * from ( select baseTable.*,rownum as rn from ("; String SQL="select u.id,u.name,u.password,u.sex,u.email,np.name nativePlace_name,ud.hobby_code " + "from users u left outer join Userdetail ud on u.id=ud.userid " + "left outer join nativeplace np on ud.nativeplace_code=np.code where 1=1 "; if(!Objects.equals("",qk.getName())) { SQL+="and u.name like ? "; } if(!Objects.equals("",qk.getSex())) { SQL+="and u.sex=? "; } if(!Objects.equals("",qk.getHobbyCode())) { SQL+="and ud.hobby_code like ?"; } String endSQL=" order by u.id) baseTable where rownum<=(?)*?) where rn>(?-1)*?"; List> list=JdbcTemplate.query(beginSQL+SQL+endSQL, (rs)->{ List> list0=new ArrayList<>(); try { while(rs.next()) { Map map=new HashMap<>(); for(int i=1,len=rs.getMetaData().getColumnCount();i<=len;i++){ map.put(rs.getMetaData().getColumnName(i), rs.getObject(i)); } list0.add(map); } } catch (SQLException e) { log.debug("query by page method error,message is "+e.getMessage()); } return list0; }, params.toArray()); page.setPageData(list); } ``` 查询基础上的分页操作 主要的思路是把查询提交和分页提交合并,每次提交后都从后台获得查询数据并填入相应的位置。以保证分页时每次提交的数据除了currentPage外都相同。以下是JS中的核心分页代码。 ```js function pagination(event){ event.preventDefault(); let nextPage=0; let currentPage=document.getElementById("queryCurrentPage").value; if(event.target.className=="btn_page"){ switch(event.target.innerHTML){ case "首页":nextPage=1;break; case "下一页":nextPage=Number.parseInt(currentPage)+1;break; case "上一页":nextPage=Number.parseInt(currentPage)-1;break; case "尾页":nextPage=document.getElementById("spanTotalPage").innerHTML;break; } }else if(event.target.id=="jumpPage"){ nextPage=document.getElementById("jumpPage").value; } document.getElementById("queryCurrentPage").value=nextPage; document.getElementById("queryForm").submit(); } ``` #### 进一步的思考提高 能不能在分页代码中抽象出可以应用在所有查询中的公共分页模块。有余力的同学可以进行思考这个问题,并尝试着做一下。 ### 12,过滤器 引入过滤器的概念,并在项目中加入过滤器,以演示过滤器的实际作用。 1,编码转换过滤器 2,session验证过滤器 ### 13,主页左侧菜单任务 完成主页面左侧,文章分类菜单设计和用户权限管理 #### 文章分类菜单 1,设计数据表,数据表的设计核心是parentId,只有有了parentId才能出现父子节点。 ```plsql create table articleType ( id number(10) not null, typeName varchar2(50), parentId number(10), --为0为父节点,为id时为子节点,id为父节点的id url varchar2(200), remark varchar2(100), constraint PK_ARTICLETYPE primary key (id) ); ``` 2,在服务器中分别获取父节点集和子节点集合。 3,在页面复用``完成父节点循环,在循环内部启动子循环,遍历子节点,显示的子节点,必须符合公式:`subArticleType.parentId==parentArticleType.id`。这样,每个父节点,都可以遍历出相应的子节点。 ```java ``` #### 有关文章分类菜单的进一步思考 本项目中的文章分类菜单是两层结构,所以可以用双层循环来完成。考虑如果要构造一个三层、四尾、N层的结构,应该如何思考、如何编码? 大致的思路:节点中包含所有子节点的集合、递归。 返回一个List,其中的每个元素是一个ArticleType类型,而ArticleType类型中,又包含一个List集合,该集合是该类型的所有子类型的集合。 ```java public class ArticleType { private int id; private String name; private int parentId; ... private List subArticleTypes=new ArrayList<>(); //存储该节点的子节点 ``` 为List集合添加数据: ```java public List getAllType() { List parentTypes=articleTypeDao.getParentType(); //获取父节点 List subTypes=articleTypeDao.getSubType(); //获取所有子点 parentTypes.forEach((parentNode)->{ setSubNode(parentNode,subTypes); }); return parentTypes; } //核心功能,为每个父节点添加内部子节点 private void setSubNode(ArticleType parentNode,List subTypes) { subTypes.stream() .filter((subNode)->subNode.getParentId()==parentNode.getId()) .forEach((subNode)->{ //setSubNode(subNode,subTypes); //如果超过两级,则使用递归,可以实现无限极 parentNode.getSubArticleTypes().add(subNode); }); } ``` ### 14,用户登录状态记录任务 完成一个用户登录时间记录,记录到数据库中,利用Filter完成。 1,建立online表,记录用户登录时间。 2,建立过滤器,在适当的位置过滤相关数据,存入数据库。 3,该任务要求在fork我的主任务之后完成,完成了Pull Requests功能,向我发送拉取请求。 ### 15,文章相关模块任务 1,创建文章信息表 2,添加文章内容流程,要使用之前准备富文本编辑器。 此处由于涉及到userId和typeId,所以之前的部分内容可以需要更新。主要指的是如何获取userId。 3,按照图片要求,显示文章类型页面。(CSS美化) 文章显示页面需要有分页功能。 4,文章类型信息编辑页面 可以修改、新增、父类型、子类型,不需要实现删除功能。文章类型不能随便删除,或者只能在没有该类型的子类型及相应类型的文章的情况下才可以删除。 ### 16,监听器 在线用户状态校验 1,完成用户重复登录,之前登录的用户被踢掉的功能。使用ServletContext类。 2,用户被踢掉时,给用户相应提示并且立即返回到登录页面。 ### 17,用户权限管理模块任务 创建基于页面粒度的用户权限管理功能,将主页左侧“管理菜单”纳入用户权限管理的范围之内,只有授权用户才可以看到并操作相应的菜单项,非授权用户无法看到更无法操作相应的菜单项。菜单项中的每个子项对应一个页面。 该模板共涉及五张表,除了users表之外,还包括:角色表、权限表、用户角色表和权限角色表