一、后端渲染 Thymeleaf

  虽然现在很多开发,都采用了前后端完全分离的模式,即后端只提供数据接口,前端通过 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: HTML

  prefix:指定模板所在的目录

  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=true

  3.创建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.hx.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>

七、数据库连接监控利器 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 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 规范编写即可。




    <meta charset="UTF-8">
    <title>聊天</title>


<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>
    

  练习:使用 websocket 与 vue.js 完成聊天室,要求完成单发和群发操作。

  参考代码:

  前端页面:




    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">


<div class="container">
    <div class="row">
        <div class="col-xs-12">
            
            
            
        </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">
            
        </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>

  后台代码:

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 &amp;&amp; fromSession.isOpen()) {
                    fromSession.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo));
                }
                return;
            }

            for (String s : routeTab.keySet()) {
                Session session = routeTab.get(s);
                if (session != null &amp;&amp; session.isOpen())
                    session.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo));
            }
        } else {//有目标 to
            String to = messageVo.getTo();
            Session toSession = routeTab.get(to);
            if (toSession != null &amp;&amp; toSession.isOpen()) {
                toSession.getBasicRemote().sendText(JacksonUtil.writeJsonStr(messageVo));
            }
            String from = messageVo.getFrom();
            Session fromSession = routeTab.get(from);
            if (fromSession != null &amp;&amp; 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------");
    }
}

十一、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.keystore

  alias:别名 这里起名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="生成证书时的口令"></connector>

  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]</testkey>

  将生成的 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更具有优越性。

十三、项目考核

项目考核:公司门户系统(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

Logo

前往低代码交流专区

更多推荐