通过项目的实现,贯穿前面所学的知识点,能够灵活应用所学知识,解决实际问题。知识点是固定的,但是实现思路是各有千秋的。
学习重点:
1.知识点的项目中的应用
2.功能实现的逻辑思路
知识点复习
1.数据库
2.JDBC
3.html
4.servlet
5.JSP+EL+JSTL
6.AJAX
7.多表操作
项目实现过程
1.分析静态页面,根据html或需求创建数据库
2.填写测试数据
3.创建项目,包结构(bean,dao.impl,servlet,service.impl,util),修改html页面为jsp
html->jsp
(1)在HTML中添加page指令
(2) 将html的后缀改成jsp
4.填写内容(注意填写顺序)
bean:属性,封装方法,无参构造,全参构造
表名=类名 ,列名=属性名
dao包:操作方法的接口,命名:实体类名+Dao,
Dbutils(属性文件,*.properties)
impl包:命名: 接口名+Impl
实现接口,继承Dbutils
service.impl
service定义的是接口,接口中的方法和dao层接口中的方法一致
impl : 这层的实现类主要负责调取dao层方法
servlet:
//1.接受参数
//2.调取service层的方法,service又在调取dao层的方法
//3.根据结果跳转页面
核心:jsp页面负责发送请求和展示数据
代码优化
(1)通用分页
定义分页工具类,简化分页代码
(2)优化servlet
实现同一个模块的请求走同一个servlet,减少定义servlet的个数
注意点
1. 登录功能
1 | //1.接收参数 |
2. 退出功能
1 |
|
注意:此处的页面跳转没有采用resp.sendRedirect("login.jsp")
的重定向方法,而是使用给页面插入js语句的方式
因为:
页面由三部分组成,退出按钮在head部分,只能对head部分进行跳转!!!
对于top.location.href='login.jsp'
,没加top的话就和重定向方法一样,加了top就是回到整个框架的最外层
3. 获取学生列表
1 | @WebServlet(urlPatterns = "/Educational/student/getStudentList") |
为什么这里的request地址用路径列表来表示
答:
相对路径/Educational/student
对url进行了自动补全,此时再要用转发或者重定向跳转到list.jsp
时就只需要直接写本地路径下的页面!!
4. 模糊查询+分页(Dao层)
在简单的数据库查询基础上,用StringBuffer来拼接sql语句,如果有查询信息就拼接到sql语句中
1 | public List<Student> getStudents(String name,String stuno,int sex,int pageIndex,int pageSize) { |
5. 新增学生
5.1 年级信息
Grade类:gradeId和gradeName
Grade类的Dao层
1 | public class GradeDaoImpl extends DBUtils implements GradeDao { |
web层
1 | "/Educational/student/getGradeList") (urlPatterns = |
如何将gradeName放到前端的select标签
里面展示
1 | <select name="gid"> |
通过java标准标签库的方式对列表进行循环,将所有年级对象的名称和对应的id填入,方便学生表和年级表的联查
5.2 学生对象
直接将前端提交的信息存储到数据库中,通过executeUpdate()
方法的返回值(受影响行数)判断是否新增成功,新增成功则跳转至学生显示页面,没成功则弹框
1 | "/Educational/student/addStu") (urlPatterns = |
6. 主键查询对学生信息进行修改
通过“修改”项的a标签给后台传输主键id
1 | <c:forEach items="${stulist}" var="stu" > |
通过主键id获取学生信息
1 | "/Educational/student/findbyid") ( |
点击修改后需要先显示学生的原本信息:(这里仅以性别和年级为例)
1 | <tr> <td>班级<span style="color:red">*</span>:</td> <td> <select name="gid"> <c:forEach items="${glist}" var="g"> <option value="${g.gradeId}" ${g.gradeId==stu.gid?'selected':''}>${g.gradeName}</option> </c:forEach> </select> </td></tr><tr> <td>性别<span style="color:red">*</span>:</td> <td> <input type="radio" name="sex" ${stu.sex==1?'checked':''} value="1" />男 <input type="radio" name="sex" ${stu.sex==0?'checked':''} value="0"/>女 </td></tr> |
7. 在6的基础上进行修改
这里就和5新增学生差不多了,通过在sql语句中用加入id条件定位学生信息,对其进行update操作
8. 删除学生
和6中修改请求的提交内容相同
1 | <a href="/Educational/student/deletebyid?sid=${stu.stuId}">删除</a> |
原理还是通过stuid删除数据库中的学生信息,将删除学生的state属性值设为4,通过受影响行数判断是否删除成功
1
public int delStu(String sid) { int update = 0; try { String sql="update student set state=? where stuid=?"; List params=new ArrayList(); //通过枚举的方式记录学生状态值,下节介绍 params.add(StudentEnum.DELETE.type); params.add(sid); update = update(sql, params); } catch (Exception e) { e.printStackTrace(); } finally { closeAll(); } return update; }
删除后重新跳转回显示界面,此时应对之前的搜寻学生的拼接sql语句(
getStudents()
和total()
)进行更新1
StringBuffer sqlbuf=new StringBuffer(" select count(*) from student where 1=1 and state!=4 ");
保证只显示未删除的数据(此时删除数据仍存在于数据库)
9. 枚举值保存学生状态
1 | //1表示在读 2 休学 3 退学 4 删除 |
10. 使用过滤器处理乱码
详见filter章节,对所有的请求和响应统一设置编码
11. 多个请求走同一个servlet
对于学生操作的servlet类很多,如何可以将这些方法走同一个servlet呢
对每一个request请求的内容做些修改
1 | <a href="/Educational/student/findbyid?sid=${stu.stuId}">修改</a> |
此时就可以将AddStuServlet、DeleteByIdServlet、GetStudentServlet、FindByIdServlet 、UpdateStuServlet可以合并为一个StudnetServlet
1 |
|
12. 分页工具类
和学生的枚举类类似,在代码直接出现state的数字或者说对每页size的定义都是很难让人理解的,通过一个额外的类对其进行整理是能极大的帮助数据的调整和别人的理解!!!
1 | public class PageUtil { //1.页码值 2.显示条数 3.查询结果列表 4,总条数 5.总页数 private Integer pageIndex; private Integer pageSize=5; private List dataList; private Integer total; private Integer totalPages; public Integer getPageIndex() { return pageIndex; } public void setPageIndex(Integer pageIndex) { this.pageIndex = pageIndex; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public List getDataList() { return dataList; } public void setDataList(List dataList) { this.dataList = dataList; } public Integer getTotal() { return total; } public void setTotal(Integer total) { this.total = total; } public Integer getTotalPages() { return total%pageSize>0? total/pageSize+1:total/pageSize; } public void setTotalPages(Integer totalPages) { this.totalPages = totalPages; }} |
此时就可以对前端传过来的数据进行封装,后端存储的数据也可以通过对象传到前端(显示的总数total会发生变化)
项目进阶-多表
1. 数据库的多表合并
表:grade,menu,middle(角色和菜单的中间表,关联两者),role,student,users
用户表:通过外键关系(roleid)和角色表关联
角色表:状态1表示可用,0表示禁用
菜单表:upmenuid表示菜单等级,0表示最上级菜单,1表示教务中心的2级菜单,2表示权限管理的2级菜单
中间表:roleid为1表示管理员权限,对应菜单表的所有信息,roleid为2表示班主任,对应教务中心和学生管理两个信息
2. 实体类创建
注意在实体类中体现多表关系:
用户表:一个用户对应一个角色
1
class Users{ private Integer userId; ... private Role role;}
角色表:一个角色可以有多个用户,一个角色也可以管理多个表单
1
class Role{ private Integer roleId; ... private List<Users> usersList; private List<Menu> menuList;}
菜单表:一个菜单可以由多个角色的管理者访问,一个菜单可能包含多个二级菜单
1
class Menu{ private Integer menuId; ... private List<Role> roleL; private List<Menu> secondMenuList; }
3. 用户列表展示
和上面的学生列表的展示方式差不多,通过在url中添加属性method,只需要一个servlet进行管理
1 | WebServlet(urlPatterns = {"/power/user/users"}) |
Dao的逻辑代码
1 |
|
4. 新增人员思路
5. 角色的新增,删除,修改
新增:先将菜单列表的内容添加到html页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57"/power/role/roles"}) (urlPatterns = {
public class RoleServlet extends HttpServlet {
private RoleService roleService=new RoleServiceImpl();
private MenuService menuService=new MenuServiceImpl();
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getParameter("method");
if("select".equals(method)){
select(req,resp);
}else if("selectmenus".equals(method)){
selectmenus(req,resp);
}else if("insert".equals(method)){
insert(req,resp);
}
}
//新增角色
protected void insert(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String rolename = req.getParameter("rolename");
String state = req.getParameter("state");
String[] menuids = req.getParameterValues("menuid");
int i = roleService.insertRole(rolename, state, menuids);
if(i>0){
resp.sendRedirect("/power/role/roles?method=select");
}else{
resp.sendRedirect("/power/role/roles?method=selectmenus");
}
}
//查询菜单列表
protected void selectmenus(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.接收参数
//2.调取service方法
List<Menu> menuList = menuService.getMenuList();
req.setAttribute("menulist",menuList);
req.getRequestDispatcher("add.jsp").forward(req,resp);
}
//查询数据(分页)
protected void select(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.接收参数(分页参数+模糊查参数)
String index = req.getParameter("index");
int pageIndex=(index==null||index.length()==0)?1:Integer.parseInt(index);
//2.调取service方法(1.查询数据列表的方法 2.查询总条数的方法)
PageUtil pageUtil = new PageUtil();
List<Role> roleList = roleService.getRoleList(pageIndex, pageUtil.getPageSize());
int total = roleService.total();
pageUtil.setTotal(total);
pageUtil.setPageIndex(pageIndex);
pageUtil.setDataList(roleList);
//3.存值跳页面
req.setAttribute("pi",pageUtil);
req.getRequestDispatcher("list.jsp").forward(req,resp);
}
}修改
删除
注意点:先删除中间表,去除主外键关系后再删除角色表
6. 不同角色的菜单进行动态更新
Dao层的用户类中,通过三表联查建立关系后的查询结果就是和用户权限菜单对应的结果,此时将菜单传到前端就可以了
3. 快递站项目
整体框架的建立/整个项目的数据流向:
MVC包:迎宾/前台
- ResponseType(枚举类型):TEXT/VIEW,记录方法的处理对象是文本还是图像
ResponseBody、ResponseView(注释):通过注释名区分ResponseType,通过注释的value对应请求的url
DispatcherServlet类(HttpServlet实现类):负责前端请求的接收和通信,在init方法中对初始化参数(application.properties文件)转换为输入流,通过反射获取其中的操作类(Controller)的带注释的操作方法
- HandlerMapping类(映射池):通过参数文件(application.properties接收操作类的类名),通过操作类的类名+反射获取到:方法的注释value(对应的请求url字符串)、方法(Method m)、ResponseType。用内部类MappingMVC接收(方法所在类Object,方法对象,响应类型),用
Map<String,MVCMapping> data
将请求URL和MappingMVC对应起来!!!!
此时,通过
HandlerMapping.MVCMapping mapping = HandlerMapping.get(uri);
我就可以在servlet中获取到执行响应请求的方法。controller包:包厢部/住房部主管
- AdminController和ExpressController:两个类分别保存了对应管理员请求和快递请求的处理方法;
此时的controller为了获取到前端请求的数据,需要访问数据库,这是一个曲折个过程。。。
首先访问service包
service包:服务员
- AdminService和ExpressService:包含了对数据库操作的意愿,但是它还碰不到数据库
此时的service中的方法只是在调用dao包的抽象方法,具体的数据库操作还是得有dao层进行
Dao包:厨子
- impl包:实现类
- 抽象类
工具(util包)和过滤器(filter包):
- DateFormatUtil:时间格式转换
- DruidUtil:德鲁伊连接池
- JSONUtil:字符串转JSON
- RandomUtil:随机数生成
- UserUtil:通过session获取用户信息
3.1 编写MVC框架
核心:注解和反射
MVC框架的意义:
以注册,登录,用户信息获取等几个功能为例,每个功能的实现可能都需要一个servlet类进行处理。框架的存在就是在一个类中对所有功能的方法进行整合!!!
大概框架:
在xml配置文件中对
*.do
的请求配置进入Servlet,在Servlet的init方法中:加载配置文件、建立映射地址的池(通过映射器来调用方法)
后台src中的文件:
application.properties:配置用于处理请求的类,每个类可能包含0-n个用于处理请求的方法
3.1.1 具体代码
xml文件的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.kaikeba.mvc.DispatcherServlet</servlet-class>
<init-param>
<param-name>contentConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>Servlet需要处理的信息:(初始化init后就是要对输入流is中的参数进行拆分获取【遍历类,类的反射对象,类中所有方法,按照方法注解的请求地址封装方法。。。】,处理完后才进行service编写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class DispatcherServlet extends javax.servlet.http.HttpServlet {
//初始化,加载配置文件(通过在xml中给配置文件起的名字获取)
public void init(ServletConfig config) throws ServletException {
String path = config.getInitParameter("contentConfigLocation");
//通过类加载器将配置文件转换为字节输入流
InputStream is = DispatcherServlet.class.getClassLoader().getResourceAsStream(path);
//这里的HandlerMapping就是自己构建的一个映射器
HandlerMapping.load(is);
}
//doGet和doPost方法都是走service方法,可以直接在这里编写
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. ********第一件事:获取用户请求的uri /xx.do
String uri = req.getRequestURI();
HandlerMapping.MVCMapping mapping = HandlerMapping.get(uri);
if( mapping == null){
resp.sendError(404,"MVC:映射地址不存在:"+uri);
return;
}
Object obj = mapping.getObj();
Method method = mapping.getMethod();
Object result = null;
try {
//参考反射中方法的执行,第一个参数就是方法所在的对象!!
result = method.invoke(obj, req, resp);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
switch (mapping.getType()){
case TEXT:
resp.getWriter().write((String)result);
break;
case VIEW:
resp.sendRedirect((String)result);
break;
}
}
}实际的操作方法中,返回值应该都为String类型,因为
result
的处理流程中(上面代码最后几行),无论前端展示还是重定向都是要转为String类型注解的建立
给用于向前端返回文字的方法建立注解
1
2
3
4
5
6
7
8
9
10
11(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会以文字形式返回到客户端
*/
public ResponseBody {
String value();
}用于跳转页面的方法建立注解
1
2
3
4
5
6
7
8
9
10
11(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会直接重定向
*/
public ResponseView {
String value();
}
注解的使用1:建立枚举类型描述注解(?注解的用处不太明白,需要关注下)
1
2
3public enum ResponseType {
TEXT,VIEW;
}Servlet和注解建立后,就是创建映射器,加载相应的方法
可以看出,映射器的本质就是一个Map集合,是对请求(String)和方法(MVCMapping)的一个映射,当然MVCMapping中不止封装了方法,还有该方法所在的类反射得到的对象,还有该方法的作用类型
问:Map中的String值 ((ResponseBody) annotation).value() 是什么意思
答:对于value方法没加默认值(default)的注解,在声明注解时必须传入参数,这时候在每个方法的注解中传入网址了,也就是Map中的String。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68/**
* 映射器(包含了大量的网址与方法的对应关系)
*/
public class HandlerMapping {
private static Map<String,MVCMapping> data = new HashMap<>();
public static MVCMapping get(String uri){
return data.get(uri);
}
public static void load(InputStream is ){
Properties ppt = new Properties();
try {
ppt.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取配置文件中描述的一个个的类
Collection<Object> values = ppt.values();
for (Object cla:values) {
String className = (String) cla;
try {
//加载配置文件中描述的每一个类
Class c = Class.forName(className);
//创建这个类的对象
Object obj = c.getConstructor().newInstance();
//获取这个类的所有方法
Method[] methods = c.getMethods();
for (Method m:methods) {
Annotation[] as = m.getAnnotations();
if(as != null){
for(Annotation annotation:as){
if(annotation instanceof ResponseBody){
//说明此方法,用于返回字符串给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.TEXT);
Object o = data.put(((ResponseBody) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseBody) annotation).value());
}
}else if(annotation instanceof ResponseView){
//说明此方法,用于返回界面给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.VIEW);
Object o = data.put(((ResponseView) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseView) annotation).value());
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 映射对象,每一个对象封装了一个方法,用于处理请求
*/
public static class MVCMapping{
private Object obj;
private Method method;
private ResponseType type;
//添加构造方法和set/get方法
}
}此时通过
类名.get()
就可以获取到请求对应的方法!!!
3.2 前端准备工作
3.2.1 二维码生成
1 | 步骤: |
注意:前端内容的编写一定要小心,没事别带空格!!!
1 | <html> |
3.2.2 弹出层-layer
步骤:
1. 下载layer , 并将解压后的layer文件夹 移动到项目中
2. 引入jquery.js
3. 引入layer.js
1 | <script src="js/jquery2.1.4.js"></script> |
3.3 准备工具类
库文件:D:\idea-project\day65_javaWebTest\web\WEB-INF\lib
时间格式工具
1
2
3
4
5
6public class DateFormatUtil {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date){
return format.format(date);
}
}数据库操作工具(需先导入德鲁伊库+mysql8.0)
使用的方法:
- 直接获得与状态通道,自己对其进行处理(设置sql和占位符参数)
- 直接调用增删改查方法,传入sql语句和参数列表(查询返回值是结果集,更新返回结果是被修改行数)
- 最后通过
closeAll()
关闭所有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103public class DBUtils {
//1.定义变量
private Connection connection;
private PreparedStatement pps;
protected ResultSet resultSet;
private int count;//存储收影响的行数
private static String userName;
private static String userPass;
private static String url;
private static String dirverName;
//德鲁伊
private static DruidDataSource dataSource=new DruidDataSource();
//2.加载驱动
static {
//德鲁伊
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
dirverName = bundle.getString("driverClassName");
url = bundle.getString("url");
userName = bundle.getString("username");
userPass = bundle.getString("password");
dataSource.setUsername(userName);
dataSource.setPassword(userPass);
dataSource.setUrl(url);
dataSource.setDriverClassName(dirverName);
// dataSource.setInitialSize(20);
}
//3.获得链接
protected Connection getConnection(){
try {
connection=dataSource.getConnection();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return connection;
}
//4.得到预状态通道
protected PreparedStatement getPps(String sql){
try {
pps= getConnection().prepareStatement(sql);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return pps;
}
//5.绑定参数 List保存的是给占位符所赋的值
protected void param(List list){
if(list!=null&&list.size()>0){
for (int i=0;i<list.size();i++) {
try {
pps.setObject(i+1,list.get(i));
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
//6.执行操作(增删改+查询)
protected int update(String sql,List list){
getPps(sql);
param(list);
try {
count= pps.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return count;
}
//7.查询
protected ResultSet query(String sql,List list){
getPps(sql);
param(list);
try {
resultSet= pps.executeQuery();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return resultSet;
}
//8.关闭资源
protected void closeAll(){
try {
if (connection != null) {
connection.close();
}
if (pps != null) {
pps.close();
}
if (resultSet != null) {
resultSet.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
3.4 后台对接管理员数据库
数据库:id+账号名+密码+登录IP+登录时间
- 先填写登录IP和登录时间,返回id
1 | public class AdminDaoMysql implements BaseAdminDao { |
3.5 管理员登录前后端交互
3.5.1 Ajax设置
1 | $(function(){ |
3.6 api文档
3.6.1 快递 部分
1. 用于获取控制台所需的快递数据
1 | 请求地址:express/console.do |
2. 快件列表(分页)
1 | 请求地址:express/findAll.do |
3. 根据用户的手机号,查询快递信息
1 | 请求地址:express/findByUserPhone.do |
4.快件录入
1 | 请求地址:express/insert.do |
5. 修改快递信息
1 | 请求地址:express/update.do |
6. 根据id删除快递信息
1 | 请求地址:express/delete.do |
3.6.2 快递员
1. 控制台
1 | 请求地址:courier/console.do |
2. 快递员列表
1 | 请求地址:courier/list.do |
3. 增加
1 | 请求地址:courier/insert.do |
4. 根据手机号查找
1 | 请求地址:courier/find.do |
5. 更新
1 | 请求地址:courier/update.do |
6. 删除
1 | 请求地址:courier/delete.do |