一、项目介绍
1、为方便项目预算信息的统计与查看,防止在excel表格中更改业务计算模式,使项目预算信息在系统中流转。
2、eclipse-oxygen+jdk1.8+maven+springboot+spring MVC+Mybatis+Thymeleaf+shiro
二、系统结构功能
- 1、系统目前实现的有两大更能模块,分别是项目预算模块和项目信息模块。(项目预算模块主要实现的是对项目预算表信息的CRUD【crud是指在做计算处理时的增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写。crud主要被用在描述软件系统中数据库或者持久层的基本操作功能。】,在数据录入的界面需要根据预算业务规则,对录入的数据进行计算并将结果显示在对应的数据框中),(项目信息模块实现的是预算系统中所需所有类型的增删改查)
三、数据库设计(采用MySQL数据库)
- 1、根据系统需求,将所需的数据分为了六张表,三张类型表,项目基本信息表,项目人力成本表和项目费用表。(在基本信息,人力成本和费用表中有一个相同的字段,用来将三张表中的数据相关联,一条完整的预算数据一定会有一条信息和费用表,人力成本数据可根据需要有多条数据)
四、框架设计
- 1、采用maven构建项目,以springboot框架规范代码流程走向,使用springMVC进行web支持,前端数据渲染采用thymeleaf模板引擎,资源认证与授权采用shiro框架,持久层使用mybatis,在开发过程中采用热部署模式,简化开发调试。
五、流程设计
- 1、系统依据数据库字段创建对应实体类,从页面拿到数据字段与请求转到controller控制层进行页面请求映射,controller根据service层接口调用dao层进行业务逻辑操作,在持久层中,类型表采用注解开发,其余表采用xml配置方式(在启动类上增添mapper包扫描注解)
六、项目难点
- 1、服务预算信息的录入页面中,人力成本表格内容需要根据需求在页面上进行动态增减数据表格(获取表格元素标签,在指定行中insertRow,设置插入行的属性setAttribute,最后通过innerHTML插入行的单元格信息和js函数),页面被访问时,ajax异步请求得到项目信息的类型,某些数据的生成需要根据其他生成数据的变化而变化致使原始数据的js函数繁多,数据封装为json并提交(一张表中至少有三个json对象,将其封装在一个array数组内,异步请求传递给后台处理)。
七、遇到的问题解决
- 1、重置按钮无法重置通过js计算生成的数据。 解决:在重置按钮上增添js函数,在点击重置按钮时,刷新页面οnclick="javascript:location.reload()"
- 2、页面数据请求插入数据库成功后,window.location.href='/url' 要跳转的url地址
- 3、页面数据请求插入数据库成功跳转后的页面不显示新添加的数据信息。解决:在该页面被请求时自动刷新页面
- window.onload加载页面时,判断url的参数是否有xyz,若无,则重新加载页面
- window.οnlοad=function(){ if(location.href.indexOf("?xyz=")<0 location.href=location.href+"?xyz="Math.random(); }
- 4、三元运算符处理数据为空的问题
八、项目存在的问题
- 插入数据的逻辑操作没有做事务处理,
- 前端没有请求失败的提示和处理,
- 动态插入单元格无法实现异步请求的数据联动,
- 类型表的id无法更改,
- 合同编号自动逻辑生成不了
九、系统优化设想
1、数据录入界面根据excel表格形式进行排版和js运算
2、数据查询可导出为excel表格数据并打印
3、系统设置资源权限
4、系统设计工作流,使得系统可以在线上流转
5、设计接口,关联其他系统的数据并能在本系统中通过组件获取或自动获取显示
十、系统用到的技术
- 1、restful软件架构风格(使用同一个URL,约定不同的method来实施不同的业务)
listCategory.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript">
/*将post method 改变为delete*/
$(function(){
$(".delete").click(function(){
var href=$(this).attr("href");
$("#formdelete").attr("action",href).submit();
return false;
})
})
</script>
<div align="center">
</div>
<div style="width:500px;margin:20px auto;text-align: center">
<table align='center' border='1' cellspacing='0'>
<tr>
<td>id</td>
<td>name</td>
<td>编辑</td>
<td>删除</td>
</tr>
<c:forEach items="${page.content}" var="c" varStatus="st">
<tr>
<td>${c.id}</td>
<td>${c.name}</td>
<td><a href="categories/${c.id}">编辑</a></td>
<td><a class="delete" href="categories/${c.id}">删除</a></td>
</tr>
</c:forEach>
</table>
<br>
<div>
<a href="?start=0">[首 页]</a>
<a href="?start=${page.number-1}">[上一页]</a>
<a href="?start=${page.number+1}">[下一页]</a>
<a href="?start=${page.totalPages-1}">[末 页]</a>
</div>
<br>
<form action="categories" method="post">
name: <input name="name"> <br>
<button type="submit">提交</button>
</form>
<form id="formdelete" action="" method="POST" >
<input type="hidden" name="_method" value="DELETE">
</form>
</div>
editCategory.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" isELIgnored="false"%>
<div style="margin:0px auto; width:500px">
<form action="../categories/${c.id}" method="post">
<input type="hidden" name="_method" value="PUT">
name: <input name="name" value="${c.name}"> <br>
<button type="submit">提交</button>
</form>
</div>
一种软件架构风格,设计风格,不是标准,只提供了一组设计原则和约束条件。主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更加简洁,更有层次,更易于实现缓存机制。
主流的三种web服务交互方案:REST(Representational State Transfer):描述一个架构样式的网络系统
SOAP(Simple Object Access Protocol)简单对象访问协议
XML-RPC(Remote procedure call远程过程调用)
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。
原则:客户端和服务器之间的交互在请求之间是无状态的(客户端发到服务器的请求须包含理解请求所必须的信息
分层系统:组件无法了解它与之交互的中间层以外的组件
- 2、pageHelper分页格式(在参数里接受当前是第几页start,每页显示条数size)
@GetMapping
(
"/categories"
)
public
String listCategory(Model m,
@RequestParam
(value =
"start"
, defaultValue =
"0"
)
int
start,
@RequestParam
(value =
"size"
, defaultValue =
"5"
)
int
size)
throws
Exception {
start = start<
0
?
0:start;//若
start 为负,改为0. 这个事情会发生在当前是首页,并点击了上一页的时候
Sort sort =
new
Sort(Sort.Direction.DESC,
"id"
); //设置倒序
Pageable pageable =
new
PageRequest(start, size, sort); //根据start,size,sort创建分页对象
Page<Category> page =categoryDAO.findAll(pageable); //dao根据分页对象获取结果page
m.addAttribute(
"page"
, page); //把page放在page属性里,跳转页面(在这个page对象里,不仅包含了分页信息,还包含了数据信息,可通过getContent()获取。
return
"listCategory"
;
}
在页面中,通过page.getContent遍历当前页面的category对象,在分页的时候通过page.number获取当前页面,page.totalPages获取总页面数(page.getContent()会返回一个泛型是category的集合)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<div align="center">
</div>
<div style="width:500px;margin:20px auto;text-align: center">
<table align='center' border='1' cellspacing='0'>
<tr>
<td>id</td>
<td>name</td>
<td>编辑</td>
<td>删除</td>
</tr>
<c:forEach items="${page.content}" var="c" varStatus="st">
<tr>
<td>${c.id}</td>
<td>${c.name}</td>
<td><a href="editCategory?id=${c.id}">编辑</a></td>
<td><a href="deleteCategory?id=${c.id}">删除</a></td>
</tr>
</c:forEach>
</table>
<br>
<div>
<a href="?start=0">[首 页]</a>
<a href="?start=${page.number-1}">[上一页]</a>
<a href="?start=${page.number+1}">[下一页]</a>
<a href="?start=${page.totalPages-1}">[末 页]</a>
</div>
<br>
<form action="addCategory" method="post">
name: <input name="name"> <br>
<button type="submit">提交</button>
</form>
</div>
- 3、打包部署jar,war
jar:mvn install打包 java -jar target/springboot-0.0.1-SNAPSHOT.jar运行
war:
- Application 修改为如下代码
新加@ServletComponentScan注解,并且继承SpringBootServletInitializer 。
- pom.xml修改为如下代码,主要两个改动
新加打包成war的声明:
<packaging>war</packaging>
spring-boot-starter-tomcat修改为 provided方式,以避免和独立 tomcat 容器的冲突.
表示provided 只在编译和测试的时候使用,打包的时候就没它了。
- mvn clean package打包
- 重命名war包,部署startup.bat
如果用 springboot-0.0.1-SNAPSHOT.war 这个文件名部署,那么访问的时候就要在路径上加上springboot-0.0.1-SNAPSHOT。 所以把这个文件重命名为 ROOT.war
然后把它放进tomcat 的webapps目录下。
- 4、springboot热部署
当发现任何类发生了改变,马上通过JVM类加载的方式,加载最新的类到虚拟机中
在pom增加一个依赖(spring-boot-devtools)和一个插件(spring-boot-maven-plugin)即可
- 5、错误处理
新增加一个类GlobalExceptionHandler,用于捕捉Exception异常以及其子类。
捕捉到之后,把异常信息,发出异常的地址放进ModelAndView里,然后跳转到 errorPage.jsp
package com.how2java.springboot.exception;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName("errorPage");
return mav;
}
}
jsp:
系统 出现了异常,异常原因是:
${exception}
<
br
><
br
>
出现异常的地址是:
${url}
</
div
>
- 6、配置文件(端口和上下文路径)application.properties
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
server.port=8888
server.context-path=/test
- 7、配置切换
application.properties:
spring.profiles.active=pro
application-pro.properties:
server.port
server.context-path
- 8、springboot单元测试,JPA条件查询,上传文件,Restful,JSON,Redis,ElasticSearch
单元测试:pom依赖,注解@RunWith(SpringRunner.class) @SpringBootTest(classes=Application.class) @Test
@Before @After
JPA条件查询:通过反射获取自定义的接口方法里提供的信息
上传文件:method="post",enctype="multipart/form-data"(二进制文件),
name="file"(和后续服务端对应),accept="image/*"(只选择图片)
restful:使用同一个URL,约定不同的method来实施不同的业务
JSON:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>用AJAX以JSON方式提交数据</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
</head>
<body>
<form >
id:<input type="text" id="id" value="123" /><br/>
名称:<input type="text" id="name" value="category xxx"/><br/>
<input type="button" value="提交" id="sender">
</form>
<div id="messageDiv"></div>
<script>
$('#sender').click(function(){
var id=document.getElementById('id').value;
var name=document.getElementById('name').value;
var category={"name":name,"id":id};
var jsonData = JSON.stringify(category);
var page="category";
$.ajax({
type:"put",
url: page,
data:jsonData,
dataType:"json",
contentType : "application/json;charset=UTF-8",
success: function(result){
}
});
alert("提交成功,请在springboot控制台查看服务端接收到的数据");
});
</script>
</body>
</html>
Redis:
- 9、springboot-Thymeleaf(URL,表达式,包含,条件,遍历,内置工具,CRUD和分页,vue.js,restful,热更新)
th:text="${name}"
字符串拼写:<p th:text="'Hello! ' + ${name} + '!'" >hello world</p> <p th:text="|Hello! ${name}!|" >hello world</p>
URL:th:href="@{/pbt-detail?(refId=${info.refId})}"
表达式:th:object
=
"${currentProduct}"
*{} 方式显示当前对象的属性 ${currentProduct.name}对象属性
${currentProduct.price+999}算数运算
包含:th:fragment 标记代码片段 th:replace
=
"include::footer2(2015,2018)"
达到了包含的效果,其中第二种可以传参。
除了th:replace, 还可以用th:insert, 区别:
th:insert :保留自己的主标签,保留th:fragment的主标签。
th:replace :不要自己的主标签,保留th:fragment的主标签。
条件:Thymeleaf 的条件判断是 通过 th:if 来做的,只有为真的时候,才会显示当前元素
取反可以用not, 或者用th:unless.
三元表达式<p th:text="${testBoolean}?'当testBoolean为真的时候,显示本句话,这是用三相表达式做的':''" ></p>
遍历:使用 th:each="p,status: ${ps} 方式遍历就把状态放在 status里面了,
同时还用3元表达式判断奇偶th:class="${status.even}?'even':'odd'"
status里还包含了如下信息:
index 属性, 0 开始的索引值
count 属性, 1 开始的索引值
size 属性, 集合内元素的总量
current 属性, 当前的迭代对象
even/odd 属性, boolean 类型的, 用来判断是否是偶数个还是奇数个
first 属性, boolean 类型, 是否是第一个
last 属性, boolean 类型, 是否是最后一个
<
select
size
=
"3"
>
<
option
th:each
=
"p:${ps}"
th:value
=
"${p.id}"
th:selected
=
"${p.id==currentProduct.id}"
th:text
=
"${p.name}"
></
option
>
</
select
>
<
input
name
=
"product"
type
=
"radio"
th:each
=
"p:${ps}"
th:value
=
"${p.id}"
th:checked
=
"${p.id==currentProduct.id}"
th:text
=
"${p.name}"
/>
内置工具:th:text="${#dates.format(now,'yyyy-MM-dd HH:mm:ss')}" 使用#dates这个内置工具进行格式化日期
分页与CRUD:
@Configuration
public
class
PageHelperConfig {
@Bean
public
PageHelper pageHelper() {
PageHelper pageHelper =
new
PageHelper();
Properties p =
new
Properties();
p.setProperty(
"offsetAsPageNum"
,
"true"
);
p.setProperty(
"rowBoundsWithCount"
,
"true"
);
p.setProperty(
"reasonable"
,
"true"
);
pageHelper.setProperties(p);
return
pageHelper;
}
}
restful:
@RequestMapping
(value=
"/listHero"
, method=RequestMethod.GET)