界面与安全设计
2.界面与安全设计
本篇的主要内容:
- 后端渲染 Thymeleaf
- 后端渲染 Freemarker
- 后端渲染 jsp
- 前端渲染 vue
- 原生 json 请求
- 前端渲染 easyui
- 前端渲染 layui
- 数据库监控利器 druid(自学)
- websocket 双向通讯
- rabbitmq 消息队列
- elasticsearch 全文检索(自学)
- rest 与 RestTemplate
- https 搭建
- http,tcp,udp 网络传输协议(自学)
- 自定义 session(自学)
后端渲染 Thymeleaf
发音:[taim li:f]
虽然现在很多开发,都采用了前后端完全分离的模式,即后端只提供数据接口,前端通过 AJAX 请求获取数据,完全不需要用的后端模板引擎。这种方式的优点在于前后端完全分离,并且随着近几年前端工程化工具和 MVC 框架的完善,使得这种模式的维护成本相对来说也更加低一点。但是这种模式不利于 SEO(搜索引擎优化 Search Engine Optimization),并且在性能上也会稍微差一点。
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>2.配置 thymeleaf
spring:
thymeleaf:
prefix: classpath:/templates/
cache: false
suffix: .html
mode: HTMLprefix:指定模板所在的目录
check-tempate-location:检查模板路径是否存在
cache:是否缓存,开发模式下设置为 false,避免改了模板还要重启服务器,线上设置为 true,可以提高性能。
encoding&content-type:这个大家应该比较熟悉了,与 Servlet 中设置输出对应属性效果一致。
3.编写 thymeleaf 模板文件
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html;charset=UTF-8"/>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>序号</th>
<th>标题</th>
<th>摘要</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<tr th:each="article : ${list}">
<td th:text="${article.id}"></td>
<td th:text="${article.title}"></td>
<td th:text="${article.summary}"></td>
<td th:text="${article.createTime}"></td>
</tr>
</tbody>
</table>
</body>
</html>可以看到,thymeleaf 还是比较简单的,并且最大的特点就是的标签是作为 HTML 元素的属性存在的,也就是说,该页面是可以直接通过浏览器来预览的。
练习:
1.使用 thymeleaf 模板完成用户表 User(code,name,age,tel)的增删改查。
后端渲染 Freemarker
与 thymeleaf 作用相同,在 web 开发中可能还会用到 Freemarker 后端模板引擎,他的原理和 thymeleaf 相同,只是语法上有稍微的区别。
官方文档:地址
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>2.配置 freemarker
spring:
freemarker:
template-loader-path: classpath:/templates/
suffix: .ftl
content-type: text/html
charset: UTF-8
settings:
number_format: '0.##'除了 settings 外,其他的配置选项和 thymeleaf 类似。settings 会对 freemarker 的某些行为产生影响,如日期格式化,数字格式化等,感兴趣的可以参考官网提供的说明。
3.编写 freemarker 模板文件
<html> <title>文章列表</title> <body> <h6>Freemarker 模板引擎</h6> <table border="1"> <thead> <tr> <th>序号</th> <th>标题</th> <th>摘要</th> <th>创建时间</th> </tr> </thead> <#list list as article> <tr> <td>${article.id}</td> <td>${article.title}</td> <td>${article.summary}</td> <td>${article.createTime?string('yyyy-MM-dd hh:mm:ss')}</td> </tr> </#list> </table>
</body>
</html>
后端渲染 jsp
提起 java Web 开发绕不开的一个技术就是 JSP,因为目前市面上仍有很多的公司在使用 JSP,所以本节就来介绍一下 Spring Boot 怎么集成 JSP 开发。springboot 整合 jsp 有如下几步:创建 war 项目结构。
1.添加相关依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>2.创建war包项目,并在 application.properties 文件中增加如下配置
spring.mvc.view.suffix=.jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
server.servlet.jsp.init-parameters.development=true3.创建webapp目录和/WEB-INF/jsp/index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>demo</title>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>序号</th>
<th>标题</th>
<th>摘要</th>
<th>创建时间</th>
</tr>
</thead>
<c:forEach var="article" items="${list}">
<tr>
<td>${article.id}</td>
<td>${article.title}</td>
<td>${article.summary}</td>
<td>${article.createTime}</td>
</tr>
</c:forEach>
</table>
</body>
</html>前端渲染 vue
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
官方文档:地址
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
</head>
<body>
<table id="app" border="1">
<thead>
<tr>
<th>序号</th>
<th>标题</th>
<th>摘要</th>
<th>创建时间</th>
<th>编辑</th>
</tr>
</thead>
<tr v-for="article in articleList">
<td>{{article.id}}</td>
<td>{{article.title}}</td>
<td>{{article.summary}}</td>
<td>{{article.createTime}}</td>
<td>
<button v-on:click="deleteThis(article.id)">删除</button>
</td>
</tr>
</table>
<script>
new Vue({
el: "#app",
data: {
articleList: []
},
created() {
let vm = this;
$.post("list", {}, function (response) {
vm.articleList = response;
})
},
methods: {
deleteThis(id) {
alert("删除" + id)
let arr = this.articleList;
for (let index in arr) {
let o = arr[index];
if (o.id == id) {
this.articleList.splice(index, 1)
}
}
}
}
})
</script>
</body>
</html>原生 json 请求
在大部分的 web 应用和 app 中,为了增加接口安全性,参数一般不采用表单方式传输,而采用原生 json 格式来传递参数。使用 @RequestBody 用来获取原生请求里面的内容,一般使用字符串接受或对象接收,使用对象接收时传输的必须是 json 字符串。使用原生 json 请求发送数据不支持使用 get 请求。
@PostMapping("index")
public String testHttpMessageConverter(@RequestBody User body){
System.out.println(body);
return "hello";
}使用原生 json 格式传输数据不能再使用浏览器地址栏调用接口,必须要使用 rest client 等接口调试工具。发送原生 json 格式的请求,需要设置请求头为:Content-Type 为 application/json,注意 request.getParameter/getParameterMap 都只能获取到你在 Request Parameters 中设置的键值对,是无法获取 json 对象中的值。

使用 jquery 发送原生 json 格式的请求如下。如果请求报 400 或者 415,请仔细检查参数。如果使用 axios 发送请求,默认就是原生 json 请求。
<script src=http://libs.baidu.com/jquery/2.1.1/jquery.min.js></script>
<script>
$.ajax({
url: "index",
method: "post",
contentType: "application/json;charset=utf-8",
data: JSON.stringify({"name": "张三", "id": 12}),
success: function (message) {
alert(message);
},
error: function (message) {
alert(message);
}
});
</script>浏览器发送参数格式如下:

练习:
1.使用原生 json 请求的方式完成员工系统练习,发送请求使用 axios,渲染数据使用 vue。
参考代码:
页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<div class="container" id="app">
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>编号</th>
<th>姓名</th>
<th>电话</th>
<th>年龄</th>
<th>编辑</th>
</tr>
</thead>
<tbody>
<tr v-for="e in employeeList">
<td>{{e.code}}</td>
<td>{{e.name}}</td>
<td>{{e.tel}}</td>
<td>{{e.age}}</td>
<td>
<button>修改</button>
<button>删除</button>
</td>
</tr>
<tr>
<td><input v-model="employee.code"></td>
<td><input v-model="employee.name"></td>
<td><input v-model="employee.tel"></td>
<td><input v-model="employee.age"></td>
<td>
<button @click="saveData()">保存</button>
<button @click="cancel()">取消</button>
</td>
</tr>
</tbody>
</table>
</div>
<script>
new Vue({
el: "#app",
data: {
employeeList: [],
employee: {}
},
created() {
this.loadData();
},
methods: {
loadData() {
let vm = this;
axios.post('list', {})
.then(function (response) {
console.log(response);
vm.employeeList = response.data.data;
})
.catch(function (error) {
console.log(error);
});
},
saveData() {
let vm = this;
axios.post('add', this.employee)
.then(function (response) {
vm.employee = {};
vm.loadData();
})
.catch(function (error) {
console.log(error);
});
}
}
})
</script>
</body>
</html>控制器:
import cn.hxzy.springmvc.entity.Employee;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Controller
public class IndexController {
private List<Employee> list = new ArrayList<>();@RequestMapping("add") @ResponseBody public Map add(@RequestBody Employee employee) { list.add(employee); Map<String, Object> map = new HashMap<>(); map.put("success", true); return map; } @RequestMapping("list") @ResponseBody public Map list() { Map<String, Object> map = new HashMap<>(); map.put("success", true); map.put("data", list); return map; } @RequestMapping("delete") @ResponseBody public Map delete(@RequestBody Employee employee) { Map<String, Object> map = new HashMap<>(); map.put("success", true); for (int i = 0; i < list.size(); i++) { Employee emp = list.get(i); if (emp.getCode().equals(employee.getCode())) { list.remove(i); } } return map; } @RequestMapping("update") @ResponseBody public Map update(@RequestBody Employee employee) { Map<String, Object> map = new HashMap<>(); map.put("success", true); for (int i = 0; i < list.size(); i++) { Employee emp = list.get(i); if (emp.getCode().equals(employee.getCode())) { list.set(i, employee); } } return map; }
}
前端渲染 easyui
easyui 在开发中是可谓是随处可见,由于其经典的主题及精炼的标签深受后端程序员喜爱。文档地址:示例文档

下载示例文档中 crud 案例,并通过对应的响应示例封装查询接口返回数据格式
{
"total": 10,
"rows": [
{
"id": 1,
"title": "哈哈哈",
"summary": "hhh",
"createTime": "2021-03-17T03:46:19.663+00:00"
},
{
"id": 2,
"title": "嘿嘿嘿",
"summary": "heiheihei",
"createTime": "2021-03-17T03:46:19.663+00:00"
},
{
"id": 1,
"title": "嘻嘻嘻",
"summary": "xixixi",
"createTime": "2021-03-17T03:46:19.663+00:00"
}
]
}增删改的返回格式为:
{"success": true}开发好相应接口后将下载的前端复制到项目中联调,并完成数据的 crud。
参考前端:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>jQuery EasyUI</title>
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/icon.css">
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/color.css">
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/demo/demo.css">
<script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.min.js"></script>
<script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
</head>
<body>
<h2>基本的增删改查</h2>
<table id="dg" title="数据表格" class="easyui-datagrid" style="width:700px;height:250px"
url="getUser"
toolbar="#toolbar" pagination="true"
rownumbers="true" fitColumns="true" singleSelect="true">
<thead>
<tr>
<th field="id" width="50">id</th>
<th field="title" width="50">标题</th>
<th field="summary" width="50">摘要</th>
<th field="createTime" width="50">创建时间</th>
</tr>
</thead>
</table><div id="toolbar">
<a href="javascript:void(0)" class="easyui-linkbutton" iconCls="icon-add" plain="true" onclick="newUser()">添加
</a>
<a href="javascript:void(0)" class="easyui-linkbutton" iconCls="icon-edit" plain="true" onclick="editUser()">编辑
</a>
<a href="javascript:void(0)" class="easyui-linkbutton" iconCls="icon-remove" plain="true" onclick="destroyUser()">删除
</a>
</div><div id="dlg" class="easyui-dialog" style="width:400px"
data-options="closed:true,modal:true,border:'thin',buttons:'#dlg-buttons'">
<form id="fm" method="post" novalidate style="margin:0;padding:20px 50px">
<h3>文章信息</h3>
<div style="margin-bottom:10px">
<input name="title" class="easyui-textbox" required="true" label="标题:" style="width:100%">
</div>
<div style="margin-bottom:10px">
<input name="summary" class="easyui-textbox" required="true" label="摘要:" style="width:100%">
</div>
</form>
</div>
<div id="dlg-buttons">
<a href="javascript:void(0)" class="easyui-linkbutton c6" iconCls="icon-ok" onclick="saveUser()" style="width:90px">确定</a>
<a href="javascript:void(0)" class="easyui-linkbutton" iconCls="icon-cancel"
onclick="javascript:$('#dlg').dialog('close')" style="width:90px">取消</a>
</div>
<script type="text/javascript">
var url;function newUser() { $('#dlg').dialog('open').dialog('center').dialog('setTitle', '添加用户'); $('#fm').form('clear'); url = 'save_user.php'; } function editUser() { var row = $('#dg').datagrid('getSelected'); if (row) { $('#dlg').dialog('open').dialog('center').dialog('setTitle', '编辑'); $('#fm').form('load', row); url = 'update_user.php?id=' + row.id; } } function saveUser() { $('#fm').form('submit', { url: url, onSubmit: function () { return $(this).form('validate');//校验数据 }, success: function (result) { var result = eval('(' + result + ')'); if (result.errorMsg) { $.messager.show({ title: 'Error', msg: result.errorMsg }); } else { $('#dlg').dialog('close'); // 关闭弹窗 $('#dg').datagrid('reload'); // 重新加载数据 } } }); } function destroyUser() { var row = $('#dg').datagrid('getSelected'); if (row) { $.messager.confirm('Confirm', '你确定要删除吗?', function (r) { if (r) { $.post('destroy_user.php', {id: row.id}, function (result) { if (result.success) { $('#dg').datagrid('reload'); // 重新加载数据 } else { $.messager.show({ // 显示错误信息 title: 'Error', msg: result.errorMsg }); } }, 'json'); } }); } }
</script>
</body>
</html>
前端渲染 layui
经典模块化前端框架,
由职业前端倾情打造,面向全层次的前后端开发者,低门槛开箱即用的前端 UI 解决方案。
文档地址:示例文档
创建一个table实例最简单的方式是,在页面放置一个元素 <table id="demo"></table>,然后通过 table.render() 方法指定该容器,如下所示:

上面你已经看到了一个简单数据表格的基本样子,你一定迫不及待地想知道它的使用方式。下面就是它对应的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>layui</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body>
<table id="demo"></table>
<script src="layui/layui.all.js"></script>
<script>
layui.use('table', function () {
var table = layui.table;
//第一个实例
table.render({
elem: '#demo'
, height: 312
, url: 'getUser' //数据接口
, page: true //开启分页
, method: "post"
, cols: [[ //表头
{field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left'}
, {field: 'title', title: '标题', width: 180}
, {field: 'summary', title: '摘要', width: 180, sort: true}
, {field: 'createTime', title: '创建时间', width: 280}
]]
});
});
</script>
</body>
</html>当然,为了更好的渲染,建议后端也按照 layui 的数据格式返回。
{
"msg": "",
"code": 0,
"data": [
{
"id": 1,
"title": "哈哈哈",
"summary": "hhh",
"createTime": "2021-03-17T06:17:42.426+00:00"
},
{
"id": 2,
"title": "嘿嘿嘿",
"summary": "heiheihei",
"createTime": "2021-03-17T06:17:42.426+00:00"
},
{
"id": 1,
"title": "嘻嘻嘻",
"summary": "xixixi",
"createTime": "2021-03-17T06:17:42.426+00:00"
}
],
"count": 10
}
数据库连接监控利器 druid(自学)
作为阿里巴巴最出色的数据库连接池 Druid 不但性能出色,更有其强大的数据库执行监控机制,在数据库开发和优化中起到举足轻重的作用。使用 Druid 需要加入 druid 依赖,并配置相应的连接和拦截。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql:///springboot?useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
filters: stat,wall,log4j打开下面的地址,就可以看到 druid 的监控页面。
http://127.0.0.1:8080/druid/sql.html

websocket 双向通讯
WebSocket 协议是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。
WebSocket 协议支持客户端与远程服务器之间进行全双工通信。用于此的安全模型是 Web 浏览器常用的基于原始的安全模式。 协议包括一个开放的握手以及随后的 TCP 层上的消息帧。
WebSocket 协议之前,双工通信是通过不停发送 HTTP 请求,从服务器拉取更新来实现,这导致了效率低下,同时增加不必要的服务器压力,WebSocket 解决了这个问题。springboot 整合 websocket 有以下几个步骤:
1.加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>2.配置类
注意:如果打 war 包部署到 tomcat 内运行时,则不能配置 ServerEndpointExporter bean,打成 jar 包独立运行时必须有该 bean
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}服务类
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;@Component
@ServerEndpoint(value = "/ws/{userId}")
public class ChatServer {private static Map<String, Session> routeTab = new HashMap<>(); @OnOpen public void onOpen(@PathParam("userId") String userId, Session session) throws IOException { routeTab.put(userId, session); } @OnClose public void onClose(@PathParam("userId") String userId) throws IOException { routeTab.remove(userId); } @OnMessage public void onMessage(String message) throws IOException { for (String s : routeTab.keySet()) { Session session = routeTab.get(s); session.getBasicRemote().sendText(message); } } @OnError public void onError(Throwable error) { error.printStackTrace(); }
}
前端按照 websocket 规范编写即可
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天</title>
</head>
<body>
<button onclick="send_msg()">
发个消息试一下
</button><script>
let ws = new WebSocket("ws://localhost:8080/ws/" + Math.random());
ws.onopen = function (evt) {
alert("建立连接");
};ws.onmessage = function (evt) { alert(evt.data); }; ws.onerror = function (evt) { alert(evt.data); }; ws.onclose = function (evt) { alert(evt.data); }; function send_msg(){ ws.send('发个消息试一下!!'); }
</script>
</body>
</html>
练习:使用 websocket 与 vue.js 完成聊天室,要求完成单发和群发操作。
参考代码:
前端页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12">
<br>
<br>
<br>
</div>
<div class="col-xs-4"><div class="input-group"> <input type="text" class="form-control" placeholder="请输入名字" id="userName"> <span class="input-group-addon" onclick="connectWebSocket(this)">连接</span> </div> </div> <div class="col-xs-8"> <form class="form-inline text-right"> <div class="form-group"> <div class="input-group"> <div class="input-group-addon" id="toBox" onclick="cleanUser()">发给全部</div> <input type="text" class="form-control" id="messageInput" placeholder="信息"> <div class="input-group-addon" onclick="sendMessage()">发送</div> </div> </div> </form> </div> <div class="col-xs-12"> <br> </div> <div class="col-xs-3"> <div class="panel panel-default"> <div class="panel-heading">在线用户</div> <ul class="list-group" id="userList"> </ul> </div> </div> <div class="col-xs-9"> <div class="panel panel-default"> <div class="panel-heading">聊天</div> <div class="list-group" id="messageList"> </div> </div> </div> </div></div>
<script>
let ws;
let count = 0;
let index;function connectWebSocket(obj) { let userId = document.getElementById("userName").value from = userId; ws = new WebSocket("ws://" + location.host + "/ws/" + userId); ws.onopen = function (evt) { if (index) clearInterval(index) obj.innerHTML = "已连接"; index = setInterval(sendType2, 3000) }; ws.onmessage = function (evt) { let jsonObj = JSON.parse(evt.data) if (jsonObj.type == 0) { jsonObj.message = JSON.parse(jsonObj.message); let arr = jsonObj.message; document.getElementById("userList").innerHTML = ""; for (let i = 0; i < arr.length; i++) { document.getElementById("userList").innerHTML += `<li class="list-group-item" onclick="setTo(this)">${arr[i]}</li>`; } } if (jsonObj.type == 1) { document.getElementById("messageList").innerHTML += `<li class="list-group-item">${jsonObj.from}:${jsonObj.message}</li>` } if (jsonObj.type == 2) { count = 0; } }; ws.onerror = function (evt) { console.info("出错"); }; ws.onclose = function (evt) { console.info("连接关闭"); }; } let from; let to = ""; function setTo(obj) { to = obj.innerHTML; document.getElementById("toBox").innerHTML = "发送给:" + to } function cleanUser() { to = ""; document.getElementById("toBox").innerHTML = "发给全部" } function sendMessage() { let message = document.getElementById("messageInput").value ws.send(JSON.stringify({to, from, message, type: 1})); } function sendType2() { if (count > 2) { connectWebSocket(); console.info("重连") } count++; ws.send(JSON.stringify({to, from, message: "ping", type: 2})); }
</script>
</body>
</html>
后台代码:
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;@Component
@ServerEndpoint(value = "/ws/{userId}")
public class ChatServer {private static Map<String, Session> routeTab = new HashMap<>(); @OnOpen public void onOpen(@PathParam("userId") String userId, Session session) throws IOException { routeTab.put(userId, session); MessageVo messageVo = new MessageVo(); messageVo.setType(MessageType.OPEN_OR_CLOSE.ordinal()); messageVo.setMessage(JacksonUtil.writeJsonStr(routeTab.keySet())); sendMessage(messageVo); } private void sendMessage(MessageVo messageVo) throws IOException { if (!StringUtils.hasText(messageVo.getTo())) {//发送的信息没有目标 to if (messageVo.getType().equals(MessageType.HEART_BEAT.ordinal())) {//心跳 String from = messageVo.getFrom(); Session fromSession = routeTab.get(from); if (fromSession != null && fromSession.isOpen()) { fromSession.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo)); } return; } for (String s : routeTab.keySet()) { Session session = routeTab.get(s); if (session != null && session.isOpen()) session.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo)); } } else {//有目标 to String to = messageVo.getTo(); Session toSession = routeTab.get(to); if (toSession != null && toSession.isOpen()) { toSession.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo)); } String from = messageVo.getFrom(); Session fromSession = routeTab.get(from); if (fromSession != null && fromSession.isOpen()) { fromSession.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo)); } } } @OnClose public void onClose(@PathParam("userId") String userId) throws IOException { routeTab.remove(userId); MessageVo messageVo = new MessageVo(); messageVo.setType(MessageType.OPEN_OR_CLOSE.ordinal()); messageVo.setMessage(JacksonUtil.writeJsonStr(routeTab.keySet())); sendMessage(messageVo); } @OnMessage public void onMessage(String message) throws IOException { MessageVo messageVo = JacksonUtil.readValue(message, MessageVo.class); sendMessage(messageVo); System.out.println(message); } @OnError public void onError(Throwable error) { error.printStackTrace(); }
}
RabbitMQ
我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Kafka,RocketMQ。
RabbitMQ官网提供了七种队列模型,分别是:简单队列、工作队列、发布订阅、路由模式、主题模式、RPC模式、发布者确认模式。
本案例只简单演示简单队列。
尝试在 windows 上安装 rabbitmq,如无法安装建议使用 docker 安装。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>配置文件
spring.application.name=rabbitmq-hello
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest配置序列
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public Queue helloQueue() {
return new Queue("hello");
}
}
发送信息
@Component
public class Sender {
@Autowired
private AmqpTemplate rabbitTemplate;public void send() { String context = "hello " + new Date(); System.out.println("Sender : " + context); rabbitTemplate.convertAndSend("hello", context); }
}
接受监听者
@Component
@RabbitListener(queues = "hello")
public class Receiver {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver : " + hello);
}
}测试使用
@SpringBootTest
public class HelloApplicationTests {
@Autowired
private Sender sender;@Test public void hello() throws Exception { sender.send(); }
}
从本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断的演进。
从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100 个在线用户已经很了不得,到现在坐拥10 亿日活的微信。此时,我们需要有一个「工具」来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。因此,消息队列就应运而生了。
它常用来实现:异步处理、服务解耦、流量控制(削峰)。
elasticsearch 全文检索(自学)
全文搜索引擎是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。现在主流的搜索引擎大概有:Lucene,Solr,ElasticSearch。
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>配置 es 连接信息
spring:
elasticsearch:
rest:
uris: http://192.168.2.128:9200创建实体
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;@Document(indexName = "blog") //indexName 不能包含大写
public class EsBlog {@Id //主键 private String id; @Field(analyzer = "ik_max_word") private String title; private String summary; @Field(analyzer = "ik_smart") private String content; @Field(index = false) private Double count; protected EsBlog() { } public EsBlog(String title, String summary, String content) { this.title = title; this.summary = summary; this.content = content; }
}
编写:Repository
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface EsBlogRepository extends ElasticsearchRepository<EsBlog, String> {
Page<EsBlog> findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining(String title, String summary, String content, Pageable page);
}
调用添加和查询方法
@SpringBootTest
class ElasticsearchDemoApplicationTests {@Autowired private EsBlogRepository esBlogRepository; @Test public void initRepositoryData() { //清除数据 esBlogRepository.deleteAll(); //初始化数据 esBlogRepository.save(new EsBlog("登鹤雀楼", "王之涣的的登鹤雀楼", "白日依山尽,黄河入海流。欲穷千里目,更上一层楼。")); esBlogRepository.save(new EsBlog("相思", "王维的相思", "红豆生南国,春来生几支。愿君多采撷,此物最相思。")); esBlogRepository.save(new EsBlog("静夜思", "李白的静夜思", "床前明月光,疑是地上霜。举头望明月,低头思故乡")); } @Test public void testFindDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining() { Pageable pageable = PageRequest.of(0, 20); String title = "思"; String summary = "相思"; String content = "相思"; Page<EsBlog> page = esBlogRepository.findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining(title, summary, content, pageable); System.err.println("------start------"); for (EsBlog blog : page.getContent()) { System.out.println(blog); } System.err.println("------end------"); }
}
REST
REST(RepresentationalState Transfer)是 Roy Fielding 提出的一个描述互联系统架构风格的名词。REST 定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的 Web 服务,包括使用不同语言编写的客户端如何通过 HTTP 处理和传输资源状态。
为什么称为 REST?Web 本质上由各种各样的资源组成,资源由 URI 唯一标识。浏览器(或者任何其它类似于浏览器的应用程序)将展示出该资源的一种表现方式,或者一种表现状态。如果用户在该页面中定向到指向其它资源的链接,则将访问该资源,并表现出它的状态。这意味着客户端应用程序随着每个资源表现状态的不同而发生状态转移,也即所谓 REST。
REST 成熟度的四个层次
第一个层次(Level0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
第二个层次(Level1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
第三个层次(Level2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
第四个层次(Level3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。
其中第三个层次建立了创建、读取、更新和删除(create,read, update, and delete,CRUD)操作与 HTTP 方法之间的一对一映射。根据此映射:
- 若要在服务器上创建资源,应该使用 POST 方法。
- 若要检索某个资源,应该使用 GET 方法。
- 若要更改资源状态或对其进行更新,应该使用 PUT 方法。
- 若要删除某个资源,应该使用 DELETE 方法。
HTTP请求的方法
- GET:通过请求 URI 得到资源
- POST:用于添加新的内容
- PUT:用于修改某个内容,若不存在则添加
- DELETE:删除某个内容
- OPTIONS :询问可以执行哪些方法
- HEAD :类似于GET,但是不返回 body 信息,用于检查对象是否存在,以及得到对象的元数据
- CONNECT :用于代理进行传输,如使用 SSL
- TRACE:用于远程诊断服务器
RestTemplate
SpringMVC 结合不同的 view 显示不同的数据,如:结合 json 的 view 显示 json、结合 xml 的 view 显示 xml 文档。那么这些数据除了在 WebBrowser 中用JavaScript 来调用以外,还可以用远程服务器的 Java 程序、C# 程序来调用。也就是说现在的程序不仅在 BS 中能调用,在 CS 中同样也能调用,不过你需要借助 RestTemplate 这个类来完成。RestTemplate 有点类似于一个 WebService 客户端请求的模版,可以调用 http 请求的 WebService,并将结果转换成相应的对象类型。
简单说就是:RestTemplate 简化了发起 HTTP 请求以及处理响应的过程,并且支持 REST。
下面的案例使用 springBoot 的 web 模块进行测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>执行方法
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String forObject = restTemplate.getForObject( "http://localhost/noteBook/all", String.class, new String[]{} );
System.out.println( forObject );
}restTemplate 的方法可以发送常见 rest 风格的请求
细心的的用户会发现每个方法有对应的三个重载方法
第一种和第二种的首个参数都是用 String 表示一个 URI。但它们的最后一个参数分别是 Object[] 和 Map 第三种的首个参数使用 java.net.URI 表示一个 URI。且只有两个参数
restTemplate 经常使用占位符发送请求
String 类型的URI支持占位符。比如: restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}",String.class,"42", "21");
那么最终访问的URI为:
http://example.com/hotels/42/bookings/21
但是 String 有一个小缺陷:String 形式的 URI 会被 URL 编码两次(URL encode请自行百度),这就要求服务器在获取URI中的参数时主动进行一次解码,但如果服务的提供者不这么做呢? 这时就需要使用不会使用任何编码的 java.net.URI
可以看出,使用 restTemplate 访问 restful 接口非常的简单粗暴无脑。还有其他很多类似的方法,有兴趣的同学可以参考官方 api。下面给出几个简单案例仅供参考
RestTemplate restTemplate = new RestTemplate();//get请求
//Map map = restTemplate.getForObject("http://localhost:8080/index?name={name}", Map.class, "张三");//post请求带数据
MultiValueMap<String, String> requestMap = new LinkedMultiValueMap<String, String>();
requestMap.add("name", "张三");
requestMap.add("id", "12");
//HttpEntity requestEntity = new HttpEntity(requestMap, null);
// String responseAsString = restTemplate.postForObject("http://localhost:8080/index", requestEntity, String.class);
//post 请求带自定义请求头
HttpHeaders headers = new HttpHeaders();
headers.put("name", Arrays.asList("1", "2"));
HttpEntity requestEntity = new HttpEntity(null, headers);
String responseAsString = restTemplate.postForObject("http://localhost:8080/index", requestEntity, String.class);
System.out.println(responseAsString);
https 搭建
HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议它是一个安全通信通道,它基于 HTTP 开发,用于在客户计算机和服务器之间交换信息。通俗来说,https 比 http 安全性更高,但 http 更加便捷。
采用 https 的 server 必须从 CA 申请一个用于证明服务器用途类型的证书,改证书只有用于对应的 server 的时候,客户端才信任该主机。因此使用 https 工作起来会非常麻烦。
以下是 http 与 https 的不同点:
- https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用。
- http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议
- http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443
- http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全
下面我们将使用 JAVA 自带的 keytool 生成证书,搭建我们的 https 服务器。使用 JAVA 自带的 keytool 生成的证书不是有效证书,不被浏览器信任,如果是被信任的站点,浏览器左侧会有个绿色的图标。
1.打开命令行输入
keytool -genkey -v -alias testKey -keyalg RSA -validity 3650 -keystore D:\keys\test.keystorealias:别名 这里起名testKey
keyalg:证书算法,RSA
validity:证书有效时间,10年
keystore:证书生成的目标路径和文件名,其文件夹必须存在
回车,然后会让你输入一些信息,其中秘钥库口令和密钥口令最好输入同一个,并且记下这个口令,(配置 tomcat 会用到该口令)其他的可随意填。
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" keystoreFile="你的keystore路径" keystorePass="生成证书时的口令" />cmd 输出结果如下:
C:\Users\Administrator>keytool -genkey -v -alias testKey -keyalg RSA -validity 3
650 -keystore D:\keys\test.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: 张三
您的组织单位名称是什么?
[Unknown]: 华信智原
您的组织名称是什么?
[Unknown]: 组织
您所在的城市或区域名称是什么?
[Unknown]: 中国
您所在的省/市/自治区名称是什么?
[Unknown]: 贵州省
该单位的双字母国家/地区代码是什么?
[Unknown]: ZH
CN=张三, OU=华信智原, O=组织, L=中国, ST=贵州省, C=ZH是否正确?
[否]: y
正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 3,650
天):
CN=张三, OU=华信智原, O=组织, L=中国, ST=贵州省, C=ZH
输入 <testKey> 的密钥口令
(如果和密钥库口令相同, 按回车):
[正在存储D:\keys\test.keystore]
将生成的 test.keystore 拷贝到 springboot resource 下,并配置 springboot 的配置文件
#端口号
server.port=443你生成的证书名字
server.ssl.key-store=classpath:test.keystore
密钥库密码
server.ssl.key-store-password=123456
server.ssl.keyStoreType=JKS
server.ssl.keyAlias=testKey
写好一个简单的接口即可以测试。
http,tcp,udp 网络传输协议(自学)
1.OSI七层模型简述
20世纪70年代中,为了优化数据库系统设计,支持数据库系统的访问,美国的一个互联网研究小组提出了一个结构化的分布式通信系统体系结构(共七层),他们内部称之为分布式系统体系结构(DSA),1977 年英国标准化协会向国际标准化组织(ISO)提议,为了定义分布处理之间的通信基础设施,需要一个标准的体系结构。后来,ISO 就开放系统互联(OSI)问题成立了一个专委会(TC 97, Subcomittee 16),指定由美国国家标准协会(ANSI)开发一个标准草案。1978 年 3 月,在 ISO 的 OSI 专委会在华盛顿召开的会议上,与会专家很快达成了共识,认为这个分层的体系结构能够满足开放式系统的大多数需求,而且具有可扩展的能力,能够满足新的需求。于是,1978 年发布了这个临时版本,1979 年稍作细化之后,成了最终的版本。
通俗的讲 OSI 七层模型是万能的国际标准化组织(ISO)提出的一个试图使各种计算机在世界范围内互连的理想标准,说白了理想和现实的差距就是七层模型和五层模型的差距。具体分类如下表:

七层模型的上三层归为应用层即为 TCP/IP 五层模型,五层模型的下两层归为链接层或者说实体层即为四层模型。
2.http 简介
HTTP:超文本传输协议(HyperText Transfer Protocol)是一种无状态协议,就是说客户端发送一次请求,服务器端接收请求,经过处理返回给客户端信息,然后客户端和服务器端的链接就断开了,为了维护他们之间的链接,让服务器知道这是前一个用户发送的请求,必须在一个地方保存客户端的信息。解决这个问题有 2 中解决方案,一是在客户端保存,二是在服务器端保存。
1.在客户端保存:Cookie
2.在服务器端保存:Session(session需要依靠cooke来实现)
3.在用户禁用 cookie 的限制下,只能使用 URL 重写的方式在每次请求之后附上一个键值对来保存客户端的信息。
4.隐藏表单。
请求/相应:HttpServletRequest / HttpServletResponse
弊端:服务端不会主动向客户端发送问题。
层级:http 属于应用层
3.https 简介
HTTPS:(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版。即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。
HTTP 与 HTTPS 的区别:
http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。http 的连接很简单,是无状态的,...
HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议要比 http 协议安全。
4.tcp 简介
tcp:(Transmission Control Protocol,传输控制协议)。tcp 是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。一个 TCP 连接必须要经过三次“对话”才能建立起来。
层级:tcp 属于传输层
TCP 建立过程(三次握手)
1.主机 A 通过向主机 B 发送一个含有同步序列号的标志位的数据段给主机 B,向主机 B 请求建立连接,通过这个数据段,主机 A 告诉主机 B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。
2 主机 B 收到主机 A 的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机 A,也告诉主机 A 两件事:我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我。
3 主机 A 收到这个数据段后,再发送一个确认应答,确认已收到主机 B 的数据段:“我已收到回复,我现在要开始传输实际数据了”。
这样 3 次握手就完成了,主机 A 和主机 B 就可以传输数据了。
三次握手的特点:
没有应用层的数据
SYN 这个标志位只有在 TCP 建产连接时才会被置 1
握手完成后 SYN 标志位被置 0
tcp 断开过程(四次握手)
TCP 建立连接要进行 3 次握手,而断开连接要进行 4 次
1 当主机 A 完成数据传输后,将控制位 FIN 置1,提出停止 TCP 连接的请求
2 主机 B 收到 FIN 后对其作出响应,确认这一方向上的 TCP 连接将关闭,将 ACK 置 1。
3由 B 端再提出反方向的关闭请求,将 FIN 置 1
4 主机 A 对主机 B 的请求进行确认,将 ACK 置 1,双方向的关闭结束。
由 TCP 的三次握手和四次断开可以看出,TCP 使用面向连接的通信方式,大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互,为数据正式传输打下了可靠的基础。
5.udp 简介
UDP:(User Data Protocol,用户数据报协议)
(1) UDP 是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
(2) 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
(3) UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
(4) 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
(5)UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表(这里面有许多参数)。
(6)UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
我们经常使用“ping”命令来测试两台主机之间TCP/IP通信是否正常,其实“ping”命令的原理就是向对方主机发送UDP数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。
层级:udp属于传输层
TCP与UDP区别:
TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。
通俗一点说就是:TCP/IP管发管到,UDP管发不管到。因此,在安全性方面来说,TCP/IP更具有优越性。
自定义 session(自学)
session 是 web 开发中不可或缺的角色,其弥补了 HTTP 请求的无状态缺点,使后台服务器能够准确的区分不同的客户端浏览器。session 依赖客户端浏览器的 cookie,由于 cookie 存储了session 的 ID 才使得服务端能更好的获取对应的 session,session 的 ID 使用标准的 UUID 唯一字符串,极难伪造才保证数据的安全。
但是在前后端开发中,由于存在跨域问题,浏览器在跨域情况下携带 cookie 被定义为一种高风险操作,于是便出现自定义 session。
其实 session 的原理也不复杂,每次客户端浏览器在适时的情况下往 session 中存入键值对数据,此时的 session 相当于一个 map,这个 map 存储在 Tomcat 的应用中,并提供一个唯一的访问方式 session 的 ID。客户端浏览器后面的所以请求都带上该参数。于是服务器端就能区分不同的客户端。其中的重点便是存入键值对数据,将获取键值对数据的 ID 发送给客户端浏览器,如果想自己设计一个是否也不困难。
用户登录成功后,便将用户的详细信息存入 Redis 键为一个 UUID,将 UUID 的值作为 token(令牌)发送给客户端浏览器,客户端浏览器将令牌存入浏览器缓存。并约定每次客户端浏览器发送请求时都带上该令牌,接收到请求后服务端从 Redis 中通过令牌获取到对应的用户信息。从而完成权限校验和对应的拦截。
项目考核:公司门户系统(10天)
1.挑选适当的前端页面模板并将其汉化(该模板至少有 3 张可以管理的数据表)。
2.设计数据库表(数据库设计文档,在设计时一般增加创建时间、最后修改时间、最后修改人编号、上下线、排序等)
3.将模板上的数据存入对应的数据库,并通过相应的 web 开发架构开发 controller,service,dao,entity,interceptor
4.挑选合适的后台管理页面模板,对所有的数据表进行增删改查,路径统一放在 /admin 目录下。
5.做好相应的权限控制,保证后台管理页面在未登录的情况下不能访问。
前台需求:
1.展示公司广告
2.展示公司产品
3.展示公司...
4.留言联系
后台需求:
1.留言数量、上线数量、浏览 top3、访问日志、设备、注册人数统计
2.用户管理
3.广告管理
4.产品管理
5.评论管理
最后提交文件:
1.原项目模板
2.数据库设计文档(参考文档)
3.源代码和数据库脚本
4.部署好项目的 tomcat
5.专业视频(介绍项目使用,从启动 tomcat 演示)
6.专业 ppt

浙公网安备 33010602011771号