该项目是基于MVC的Java Web基础教学项目,不涉及前端内容。相关的前端内容请访问同账号的其他项目。
该项目为至少要运行在JDK1.8环境中,建议运行在JDK 9或JDK 10中;服务器建议使用tomcat 9或Jetty 12版本。
最基础的MVC模式:JSP+Servlet+JavaBean结构,使用maven构建和管理,使用git进行版本控制。
使用JSTL/EL、原生AJAX等技术。
完成该项目需要一些首先完成一些前置任务。在掌握前置任务之后,才能完成该项目
maven是基于apache管理的进行java项目构建、jar包管理软件。主要的作用是使用最佳实践管理java项目和jar包间的依赖关系。
本项目的就是构建基于maven的web项目,所有的第三方jar包都依赖于中央仓库。
与依赖与某种IDE构建的项目不同,依赖于maven构建的项目在所有IDE中都保持相同的结构。其结构如下:
maven不需要安装,只解压就可以使用了。但使用使用前需要进行部分配置工作。
配置maven\conf\settiong.xml文件
修改本地仓库的地址;新增镜像仓库地址
<localRepository>E:\yuhf\.m2\repository</localRepository>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
Eclipse JavaEE版本默认包含maven插件。所以只需要对maven插件进行简单的配置就可以使用。
配置路径:windows->references->maven->user setting
配置主要是对maven软件的路径设置。包括全局setting.xml文件和局部setting.xml文件的指定。
配置pom.xml文件
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>edu.yuhf</groupId>
<artifactId>blogServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<webResources>
<resource>
<directory>src/main/webapp</directory>
<filtering>true</filtering>
<targetPath>WEB-INF</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.5.v20170502</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<httpConnector>
<port>9000</port>
</httpConnector>
<contextPath>/</contextPath>
<webAppConfig>
<defaultsDescriptor>src/test/resources/webdefault.xml</defaultsDescriptor>
</webAppConfig>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>12.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
<packaging>war</packaging>
</project>
学会使用Git软件的用法,并确定远程仓库。本文建议使用码云。
使用Eclipse时,不需要专门安装Git软件,直接在Eclipse中就可以使用Git的相关功能。
创建如下:java文件,完成数据库访问代码封装和优化,实现数据库连接信息软编码。 完成对一般的数据库操作(CRUD)的封装。可以返回实体类实例。 DBConnection.java
static{}
public Connection getConnection();
public void closeConnection(Connection connection);
JdbcTemplate.java
public List<T> 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
public class TransactionManager {
public void beginTranscation() {} //开启事务
public void commitAndClose() {} //提交并关闭事务
public void rollbackandClose() {} //回滚并关闭事务
}
jdbc.properties配置文件
首先在pom.xml文件中添加依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
之后添加log4j.properties配置文件
log4j.rootLogger=debug,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern= %d - %c -%-4r [%t] %-5p - %m%n
最后在每个要进行日志输出的类中的加入Logger的声明。如下:
private static Logger log=Logger.getLogger(DBConnection.class);
...
log.debug("....");
log.error("....");
增加了事务控制类,为此要对数据库连接类进行一些修改。
主要的目的是实现线程和connection引用对象的统一。关键是使用ThreadLocal类。ThreadLocal类的数据结构是Map。键是线程id,值可以放任何对象。
使用PowerDesigner软件设计数据库的物理模型。基本参照如下:
1,用户信息管理模块:用户表、用户详情表、行政区划表、爱好表 2,文章信息管理模块:文章分类表、文章信息表 3,用户权限管理模:角色表、权限表、用户角色表、角色权限表
要求完成基本的设计工作,生成pdm文件。
create tablespace nnblog
datafile 'D:\app\Administrator\oradata\orcl\nnblog.dbf'
size 100m autoextend on next 20m;
create user nnblog identified by nnblog default tablespace nnblog;
grant connect to nnblog;
grant resource to nnblog;
基于Grid,构建主页页面
创建不带数据库验证的用户登录流程。熟悉web开发中的基于MVC设计模式的开发流程。
注意,登录流程实际上包含两个相互独立的流程。
index.jsp-->init.servlet-->login.jsp 流程1
login.jsp-->user.servlet-->login.jsp或veiws/main.jsp 流程2
说明:尽量不要出现在页面直接转向另一个页面的情况。
在用户登录流程中,加入session验证、cookie校验功能。
1,session验证:用session保护所有需要身份验证才能访问的页面。
知识点:session、Filter
2,cookie验证:往访问者本机存入一个文本,称作cookie。任何网站都可以访问这些cookie。
知识点:cookie
综合知识:session和cookie区别?
3,Filter的概念和应用方法
知识点:AOP、过滤器的的应用、chain.doFilter()、过滤器链等
完成数据库的创建,创建nnblog表空间,创建nnblog用户并授权,创建users表,并添加部分数据。
//创建用户表
create table users(
id number(10) primary key,
name varchar2(50) not null,
nickName varchar2(80) not null,
password varchar2(50) not null,
sex char(1) default '1',
email varchar2(100)
);
alter table USERS add unique (NAME)
select * from users;
create sequence users_id start with 0 increment by 1 minvalue 0 maxvalue 99999999;
insert into users values(users_id.nextval,'admin','我是老大','admin','1','albert@qq.com');
insert into users values(users_id.nextval,'test','深情的维那斯','test','0','john@qq.com');
commit;
以上创建工作全部手写完成,源代码文件放入项目文件中。
加入service(业务部分)、dao(数据访问部分)及任务4中的数据库访问工具类。 service、dao都必须基于接口、且进行专门的单元测试。
UserService 接口
UserServiceImpl 实现
UserDao 接口
UserJdbcDaoImpl 实现
UserServiceImplTest
UserJdbcDaoImplTest
Dao负责访问数据库,并最终返回数据。注意返回的数据不能是ResultSet之类的java.sql中的类。一般返回的domain对象或者是集合、数组等。
Service负责业务逻辑。
所谓单元指的方法,也就是说,代码中所有的方法都有一个对应的测试方法;所有的类都要有一个对应的测试类。当然Servlet目前无法测试,在使用Spring框架之前,主要测试对象Service类中的方法和Dao类中的方法。
添加JUnit.jar依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
在测试方法上加上@Test注解。
引入过滤器的概念,并在项目中加入过滤器,以演示过滤器的实际作用。
1,编码转换过滤器 2,session验证过滤器
引入监听器的概念
1,完成用户重复登录,之前登录的用户被踢掉的功能。使用ServletContext类。
2,用户被踢掉时,给用户相应提示并且立即返回到登录页面。
完成一个用户登录时间记录,记录到数据库中,利用Filter完成。
1,建立online表,记录用户登录时间。
2,建立过滤器,在适当的位置过滤相关数据,存入数据库。
创建相应的数据库表,包括:用户详情表、爱好信息表、行政区划表
create table userDetail(
id number(10) primary key,
nativeplace_code varchar2(6) references nativePlace(code),
hobby_code varchar2(50),
userId number(10) references users(id)
);
create table hobby(
id number(10) primary key,
name varchar2(50),
code number(2) unique
);
create sequence hobby_id start with 0 increment by 1 minvalue 0;
create table nativePlace(
id number(10) primary key,
name varchar2(50) unique,
code varchar2(6)
);
alter table NATIVEPLACE add constraint unique1 unique (CODE);
1,编写用户注册页面,主要注册信息包括:用户名、昵称、密码、性别、爱好、来自于和Email。 2,在登录页面点击注册按钮,通过user.servlet?param=register转向注册页面。 3,在servlet中收集所需要的信息,包括:爱好信息,来自于的省级信息等。 4,在register.jsp页面中显示相关信息。 5,在省级下拉列表变化时,市级下拉列表显示相对应的信息。(利用ajax完成)
AJAX技术、ESM(ECMAScript Module)
6,提交form表单到服务器端 7,服务端根据提交信息写入数据库,完成用户注册。
graph LR(注册页面显示流程)
节点1(login.jsp)-->节点2(UserServlet)-->节点3(register.jsp)
graph LR(注册流程)
节点1(register.jps)-->节点2(UserServlet)-->节点3(InitServlet)-->节点4(login.jsp)
1,在UserServlet中有两个注册相关的流程控制关键字:register、doRegister
register关键字是单纯的转向;doRegister是真正的注册操作。
后面的的update和doUpdate关键字效果和这个一样。
2,存储转发和重定向的功能及区别
3,JSTL,Java自定义标签。自定义标签虽然像标签,但本质是一个Java类。
引入自定义标签
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
4,${}指的是EL表代式。EL是用来在页面展示数据的一种编程语言。
5,原生AJAX的使用和封装技术。
6,JS模块化技术IIFE、CMD、AMD、ESM。这里只讲解了ESM技术(2018年5月之后的主流浏览器开始支持)。
完成用户信息管理页面的开发任务
1,构建基础页面,显示用户信息。用户信息从users表、userDetail表及NativePlace表中获取。还要在Hobby表通过业务代码进行相应的转换处理。 利用List<Map<String,Object>>结构进行多表联合查询结果存储工作。 利用专门的业务代码替换原结构中的的编码为更友好的中文显示。
根本上是实现如下方法:
public List<Map<String, Object>> queryAll()
2,对表格进行适度的美化处理,增加删、改、查功能;增分页按钮功能。 4,完成删、改、查操作,每个一个操作都是一个完整的处理流程。注意改操作是两个流程。 5,完成分页功能的编写工作。 6,和主页连接。
返回并显示用户信息的具体解析:
1,实现三个表相互连接,具体的SQL语句如下:
select u.id,u.name,u.nickname,u.password,u.sex,u.email,np.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;
2,List<Map<String,Object>>类型对应表数据的理解
3,将某行数据放入map中的具体写法:
for(int i=1,len=rs.getMetaData().getColumnCount();i<=len;i++){
map.put(rs.getMetaData().getColumnName(i), rs.getObject(i));
}
//rs.getMetaData()方法获取rs中的元数据,其中包括了列数和每列的列名。
//rs.getMetaData().getColumnName(int index)方法返回第i列的列名(i从1开始计算),列名为大写
1,行政区划未选择时,显示列表显示红色的“未知”二字。 2,在显示列表中,行政区划一栏,改为省分,城市的写法。如:原来为“济南”,改为:“山东济南”。 3,考虑到用户起名方便,在数据库中加入昵称字段,用户名为英文、昵称字段可以录入中文信息。
在修改页中,每次自动显示爱好信息,并在已经选择的信息上打对号。
思路:在Hobby类中,加入一个表中没有的属性:private boolean selected,用以说明某个用户的选择。之后将Hobby表中的数据和用户数据中的hobby_code数据迭加,为用户选择了的数据,添加selected为真。显示时,把为真的数据显示为checked,就可以了。
修改页面中显示用户来自的区域,同时将用户已经选择的区域作为选中区域显示。
思路:与爱好类似,在NativePlace类中加入表没有的属性,以存储选择状态。然后在获得地区数据时,对数据进行比对。
具体的:
@Override
public List<NativePlace> setProvinceSelected(String code) {
List<NativePlace> 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<NativePlace> setCitySelected(String code) {
List<NativePlace> list=nativePlaceDao.getLevel2(code.substring(0, 2));
list.stream().filter((item)->
Objects.equals(item.getCode(),code)).forEach((item)->item.setSelected(true));
return list;
}
1,分页准备,了解Oracle数据库分页代码的原理,掌握Oracle数据库分页的编码方式。
2,创建Page类,封装分页相关数据。
3,创建分页方法,一般原型为:queryByPage(Page page);
4,初始页面分页
5,上、下页切换功能(主要难点在前端)
6,跳页功能的实现
难点主要有两个:
实现多字段查询,这需要灵活的操作数据库,并根据查询字段的组合进行sql语句的拼装。大致的代码如下:
public void queryByPage(Page<List<Map<String, Object>>> page, UMQueryKeyword qk) {
List<Object> 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<Map<String,Object>> list=JdbcTemplate.query(beginSQL+SQL+endSQL, (rs)->{
List<Map<String,Object>> list0=new ArrayList<>();
try {
while(rs.next()) {
Map<String,Object> 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中的核心分页代码。
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();
}
能不能在分页代码中抽象出可以应用在所有查询中的公共分页模块。有余力的同学可以进行思考这个问题,并尝试着做一下。
完成主页面左侧,文章分类菜单设计
设计数据表,数据表的设计核心是parentId,只有有了parentId才能出现父子节点。
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)
);
1,在服务器中分别获取父节点集和子节点集合。
2,在页面复用<c:forEach>
完成父节点循环,在循环内部启动子循环,遍历子节点,显示的子节点,必须符合公式:subArticleType.parentId==parentArticleType.id
。这样,每个父节点,都可以遍历出相应的子节点。
<ul id="leftMenu">
<c:forEach items="${requestScope.parentTypes}" var="articleType">
<li class="leftMenuLi1">${articleType.name}
<div class="leftMenuLi1_image"></div></li>
<ul id="leftSubMenu">
<c:forEach items="${requestScope.subTypes}" var="subArticleType">
<c:if test="${subArticleType.parentId==articleType.id}">
<li class="leftMenuLi2">
<a class="navigate" href="${subArticleType.url}">${subArticleType.name}</a>
</li>
</c:if>
</c:forEach>
</ul>
</c:forEach>
</ul>
本项目中的文章分类菜单是两层结构,所以可以用双层循环来完成。考虑如果要构造一个三层、四尾、N层的结构,应该如何思考、如何编码?
大致的思路:节点中包含所有子节点的集合、递归。
返回一个List,其中的每个元素是一个ArticleType类型,而ArticleType类型中,又包含一个List集合,该集合是该类型的所有子类型的集合。
public class ArticleType {
private int id;
private String name;
private int parentId;
...
private List<ArticleType> subArticleTypes=new ArrayList<>(); //存储该节点的子节点
为List集合添加数据:
public List<ArticleType> getAllType() {
List<ArticleType> parentTypes=articleTypeDao.getParentType(); //获取父节点
List<ArticleType> subTypes=articleTypeDao.getSubType(); //获取所有子点
parentTypes.forEach((parentNode)->{
setSubNode(parentNode,subTypes);
});
return parentTypes;
}
//核心功能,为每个父节点添加内部子节点
private void setSubNode(ArticleType parentNode,List<ArticleType> subTypes) {
subTypes.stream()
.filter((subNode)->subNode.getParentId()==parentNode.getId())
.forEach((subNode)->{
//setSubNode(subNode,subTypes); //如果超过两级,则使用递归,可以实现无限极
parentNode.getSubArticleTypes().add(subNode);
});
}
1,创建文章信息表
2,添加文章内容流程,要使用之前准备富文本编辑器。
此处由于涉及到userId和typeId,所以之前的部分内容可以需要更新。主要指的是如何获取userId。
3,按照图片要求,显示文章类型页面。(CSS美化)
文章显示页面需要有分页功能。
4,文章类型信息编辑页面
可以修改、新增、父类型、子类型,不需要实现删除功能。文章类型不能随便删除,或者只能在没有该类型的子类型及相应类型的文章的情况下才可以删除。
RBAC(Role-Based Access Control),基于角色的访问控制。根据复杂度,可分为RBAC0、RBAC1、RBAC2、RBAC3。本模块使用RBAC0。RBAC0模型中角色可以理解为把权限分组。
创建基于页面粒度的用户权限管理功能,将主页左侧“管理菜单”纳入用户权限管理的范围之内,只有授权用户才可以看到并操作相应的菜单项,非授权用户无法看到更无法操作相应的菜单项。菜单项中的每个子项对应一个页面。
该模块共涉及五张表,除了users表之外,还包括:角色表、权限表、用户角色表和权限角色表
该模块共涉及三个页面,分别是:用户信息管理页面(作为用户信息模块的一部分已经完成);角色信息管理页面;权限信息管理页面。
利用Filter在转向到views/main.jsp时,对用户名进行验证,并根据用户名筛选相应权限。核心代码如下:
--根据用户名获取该用户权限
select p.permission,p.url from users u left outer join userRole ur on u.id=ur.userId
left outer join permissionRole pr on ur.roleid=pr.roleId
left outer join permission p on pr.permissionId=p.id
where u.name='admin';
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request =(HttpServletRequest)req;
HttpSession session=request.getSession();
if(session.getAttribute("name")!=null) {
PermissionService permissionService=new PermissionServiceImpl();
List<Permission> permissions=permissionService.
getPermissions((String)session.getAttribute("name"));
session.setAttribute("permissions", permissions);
}
chain.doFilter(req, response);
}
###test
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。