LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

Vue

2024/7/3

Vue快速入门

简介

MVVM响应式编程模型,避免直接操作DOM,降低DOM操作的复杂性

安装Vscode 安装nodejs
检测是否安装成功 cmd → node -v → npm -v

npm设置镜像[cmd]
C:\Users\Pluminary>npm config set registry https://registry.npmmirror.com
C:\Users\Pluminary>npm config ls
C:\Users\Pluminary>npm config get registry
在Vscode里的终端输入 
PS C:\Users\Pluminary\Desktop\vue2> npm init -y
PS C:\Users\Pluminary\Desktop\vue2> npm install vue

Vue Chrome调试工具 Vue.js devtools 5.3.3 安装包及教程_vuedevtool.crx百度网盘-CSDN博客

简单案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<!--<div>{{name}}</div> 这个就没有被挂载-->
    <div id="app">
        <input type="text" v-model="num"> <!--在对话框里输入的时候改变下面的次数-->
        <button v-on:click="num++">登录</button> <!--实现当点击按钮的时候num数值++-->
        <button v-on:click="loginout()">登出</button>
        <h1>{{name}}, 欢迎您,当前登录第{{num}}次,当前登录时间:{{nowDate()}}</h1>
    </div>
    <script src="./node_modules/vue/dist/vue.js"></script>

    <script>
        // 1. new vue实例 每个Vue应用都是通过Vue函数创建一个新的Vue实例开始的
        new Vue({
            el:"#app", // 挂载的模板
            data(){ // 绑定的数据 把data对象中的所有属性 加到相应视图里
                return{
                    name : "图灵学院",
                    num:1
                }
            },
            methods: {
                nowDate(){
                    return new Date().toLocaleDateString()
                },
                loginout(){
                    this.num--; //当调用实例时要加this
                }
            },
        });
        // 双向绑定:数据发生改变 视图也要随之改变;在谷歌浏览器的Vue调试中
        // 指令:简化对Dom的频繁操作
        // 方法:声明方法可以实现更复杂的操作,声明methods属性中
    </script>
</body>
</html>

指令 v-text、v-html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-text="message"></div>{{message}}
        <!-- {{}}在网络延迟情况下会出现暂时显示的情况 -->
        <div v-html="message"></div>
    </div>
</body>
<script>
    // 1.实例Vue
    var vm = new Vue({
        el:'#app',
        data:{
            message: "<a href = '#'>Hello</a>"
        },
    })
// v-text 用于绑定数据,语法v-text="属性",会直接将值作为文本显示
// v-html 会将值进行编译再显示
</script>
</html>

指令 v-bind

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .red{
            background-color: red;
        } 
        .yellow{
            background-color: yellow;
        }
    </style>
</head>
<body>
    <div id="app">    <!--:style="font-size: {{bigFont}}; 错误写法-->
        <div v-bind:title="title" :class="red" :style="{fontSize:bigFont}">
            鼠标悬停查看信息!
        </div>
        <div :class="{yellow:isyellow}">
            <a :href="href" :style="{fontSize:bigFont}">图灵学院</a>
        </div>
        <div>
            <img :src="src"/>
        </div>
        <div>
            <button v-bind:disabled="disabled">禁用按钮</button>
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data:{
            red:"red",
            isyellow:true,
            bigFont:"50px",
            title: "您好,本网站可以学到更多的知识",
            href: "https://www.pronhub.com/",
            scr: '../Imooc_Cat.jpg',
            disabled: true
        }
    })

    // 绑定元素属性 -- v-bind
    // 语法:v-bind:元素属性="vue的属性"
    // 简写:v-bind:title 简写成 :title
    /*
       针对样式的特殊用法:
         动态控制class是否添加 :class="{red:isred}"
           语法 {red:isred} : {需要动态控制的class样式:vue的属性(需要是boolean类型)}

         动态设置style的样式   :style="{fontSize:bigFont}"
           语法 {fontSize: bigFont} : {css样式的驼峰命名方法:} font-size=fontSize
    */
</script>
</html>

指令 v-model

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div>
            <label>年龄:</label>
            <input v-model="age"/>
        </div>
        <div>当前输入的年龄是:{{age}}</div>
        <button @click="add">加一岁</button> <!--  @click = v-on:click  -->
        <button @click="alertYear">弹出年龄</button>

    <hr>
        <label>课程:</label>
        <select v-model="course">
            <option value="javascript">javascript</option>
            <option value="java">java</option>
            <option value="c++">c++</option>
        </select>

        <div>
            <input v-model="course" type="radio" name="course" value="javascript">javascript
            <input v-model="course" type="radio" name="course" value="java">java
            <input v-model="course" type="radio" name="course" value="c++">c++
        </div>

        <div>当前课程是:{{course}}</div>
    </div>
    
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el:"#app",
    data:{
        age:10,
        course:'java'
    },
    methods: {
        add(){
            this.age++;
        },
        alertYear(){
            alert(this.age)
        }
    }
})

//v-model 用于实现双向绑定 一般用在表单元素
</script>
</html>

指令 v-on

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button v-on:click="hello('hello')">hello</button>
        <button @click="hello('world')">world</button>
        <button @click="num++">数据:</button>{{num}}
<!-- 不让事件传播 禁止默认a标签事件 
 @click.stop 不让事件传播
 @click.self 需要自己点击[不会参与传播]
 @click.prevent 组织默认事件
 @click.once 事件只会触发一次
 -->
        <div @click="hello(1)" style="background-color: red; width: 800px; height: 500px;padding: 50px;">
            1
            <div @click.stop="hello(2)" style="background-color: blue; width: 400px; height: 200px;padding: 50px;">
                2
                <a @click.stop.prevent="hello(3)" style="background-color: yellow; width: 200px; height: 100px; display: block;padding: 50px;" href="www.baidu.com">3</a>
            </div>
        </div>

        <input v-on:keyup.enter="hello('enter')"> <!--键盘按回车触发--><br>
        <input v-on:keyup.space="hello('enter')">
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data:{
            num: 0
        },
        methods: {
            hello(str){
                alert(str)
            }
        }
    })

/*
 v-on用于绑定事件
 语法  v-on:事件名
 简写方式 v-on:click => @click
 只能调用vue中的函数和数据
*/
</script>
</html>

指令 v-for

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        循环数组
        <ul>
            <li v-for="(item, index) in music">{{item.name}}---{{index+1}}</li>
        </ul>
        循环对象
        <ul>
            <li v-for="(item, index) in obj">{{item}}</li>
        </ul>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: '#app',
    data:{
        // 要循环的数组
        music:[
            {name: '青花瓷'},
            {name: '阳光总在风雨后'},
            {name: '十年'}
        ],
        // 要循环的对象
        obj:{
            name: '句号',
            age: 18,
            sex: '男'
        }
    }
})
/*
v-for循环,可以循环数组和对象
v-for="(item, index) in intems
语法:v-for="(每次循环接收的变量, 当前循环的索引) in 需要循环的变量"
*/
</script>
</html>

指令 v-if 与 v-show

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button @click="vif=!vif">切换显示隐藏</button>
        <div v-if="vif" style="background-color: red; width: 200px; height: 200px;">
            v-if
        </div>
        <hr>
        <button @click="vshow=!vshow">切换显示隐藏</button>
        <div v-show="vshow" style="background-color: yellow; width: 200px; height: 200px;">
            v-show
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: "#app",
    data:{
        vif: true,
        vshow: true
    }
})
/*
    v-if 和 v-show 用于控制元素显示隐藏
    语法 v-if = "指定Boolean值" v-show一样
    
    v-if 控制元素是否生成
    v-show 控制元素是否显示隐藏
*/
</script>
</html>

指令 v-else 与 v-else-if

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-if="number == 1">
            A
        </div>
        <div v-else-if="number == 2">
            B
        </div>
        <div v-else>
            C
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: '#app',
    data() {
        return {
            number: 1
        }
    },
})
</script>
</html>

计算属性和侦听器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>计算属性和侦听器</title>
</head>
<body>
    <div id="app">
        <!-- 实现一个购物车功能 
            1. 数据初始化处理
            2. 选择商品数量:当商品数量超过库存做提示
            3. 计算商品总价
        -->
        <ul>
            <li v-for="(item, index) in car">
                {{item.pname}} --- 商品价格:{{item.price}} --- 库存:{{item.stock}}
                数量:<input type="number" v-model="item.num" style="width: 30px;"/>
            </li>
            <li><strong>总价:</strong>{{calcSum}}</li>
            <!-- <li v-html="warn()"></li> -->
            <li v-html="message"></li>
        </ul>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
 new Vue({
    el:"#app",
    data:{
        car:[
            {pname:'IPhone 12',price:10000,stock:10,num:1},
            {pname:'Mate40 pro',price:7000,stock:3,num:1},
        ],
        message:"",
    },
    // methods:当方法中的数据发生改变,方法会自动调用 所以当数量增加时会自动计算总价
    methods: {
        // calcSum(){
        //     let sum=0;
        //     this.car.forEach(item => {
        //         sum+=item.price*item.num;
        //     });
        //     return sum;
        // }

        // warn(){
        //     let message="";
        //     this.car.forEach(item => {
        //         if(item.num>item.stock){
        //             message+=`${item.pname}的库存超出限制<br/>`
        //         }
        //     });
        //     return message;
        // }
    },
    // 计算属性 上面就不加大括号  <li><strong>总价:</strong>{{calcSum}}</li>
    computed:{
        calcSum(){
            let sum=0;
            this.car.forEach(item => {
                sum+=item.price*item.num;
            });
            return sum;
        }
    },
    // 侦听器 专门用于侦听某些数据的变化,当数据发生变化会自动调节方法 不能像方法那样调用
    watch:{
        // 要侦听的数据 
        car:{
            handler(newvalue,oldvalue){
                this.message="";
                this.car.forEach(item => {
                    if(item.num>item.stock){
                        this.message+=`${item.pname}的库存超出限制<br/>`
                    }
                });
            },
            deep:true
        },
        //侦听message 基础类型的侦听 将侦听数据作为函数就可以了
        message(newvalue,oldvalue){
            console.info(newvalue,oldvalue)
        }
    }
 });
 /*
    方法和计算属性的区别:
    声明方式不一样,调用不一样方法要用"()", 计算属性调用不要加"()"
 */
/*
    方法和侦听器的区别:
        方法:方法中任意数据发生改变了就会自动调用方法 
            可以调用,进行返回值

        侦听器:需要指定具体的侦听数据,只有被具体指定的侦听数据发生了改变才会触发
            不能像方法那样去调用,而是靠vue自动触发 如果初始情况就数量就大于库存是不改变的
*/
</script>
</html>

过滤器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>过滤器</title>
</head>
<body>
    

    <div id="app">
        <ul>
            <li v-for="user in userList">
                姓名:{{user.name}}; 性别:{{user.gender==1?"男":"女"}}
                {{user.gender | filterGender}}
            </li>
        </ul>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script type="text/javascript">
    let vm = new Vue({
        el: "#app",
        data:{
            userList:[
                {id:1, name:'xushu', gender:1},
                {id:2, name:'zhuge', gender:0}
            ]
        },
        //针对数据过滤的
        methods:{
            formateGender(gender){
                if(gender==1){
                    return "~男"
                }else{
                    return "~女"
                }
            }
        },
        filters:{
            filterGender(gender){
                if(gender==1){
                    return "!男"
                }else{
                    return "!女"
                }
            }
        }
    })
</script>
</body>
</html>

Vue-组件化

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件化</title>
</head>
<body>
    <div id="app">
        <!-- <div @click='num++'>{{num}}</div> -->
        <!-- <xushu></xushu> -->
        <xushu :num="numxx"></xushu> <!--传递属性-->  <!--输出50-->
        <app-xushu></app-xushu> <!--输出66-->
    </div>

    <div id="app2">
        <!-- <div @click='num++'>{{num}}</div> -->
        <!-- <xushu></xushu> -->
        <xushu :num="numxx"></xushu> <!--传递属性--> <!--输出100-->
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        // 1.全局组件 在所有的Vue实例中使用
        /*
            1.1 命名:不要使用驼峰命名法 可以用中划线命名
            1.2 模板:必须的
            1.3 数据:一定要函数的方式声明
        */
        Vue.component("xushu", {
            template: `<div @click='num++'>{{num}}</div>`,
            //1.在自己的字方法中声明  
            // data() {
            //     return {
            //         num:10
            //     }
            // },

            //2.在下方父方法中声明并传递过来
            props: ["num"]
        });

        // 2.局部组件 //自己定义下面引用
        const appXushu = {
            template: `<div @click='num++'>{{num}}</div>`,
            data() {
                return {
                    num: 66
                }
            }
        };

        new Vue({
            el: "#app",
            data: {
                numxx: 50
            },
            components: {
                "app-xushu": appXushu
            }
        });

        new Vue({
            el: "#app2",
            data: {
                numxx: 100
            }
        });
    </script>
</body>
</html>

生命周期和钩子函数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <span id="name">{{name}}</span>
        <button @click="updateName">更新</button>
        <button @click="destroyInstance">销毁</button>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data:{
            name: 'hello !'
        },
        methods: {
            updateName(){
                console.log('准备改名字啦!')
                this.name = 'hello 图灵!'
            },
            destroyInstance(){
                console.log('销毁实例')
                vm.$destroy()
            }
        }
    })
</script>
</html>

vue-router

npm install vue-router@3
npm install vue-router@3 --legacy-peer-deps

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<!-- 1.安装vue-router路由
     2.引入vue-router文件
     3.根据不同url连接到不同的页面,需要使用模板实现,将模板绑定对应的路由地址
-->
<body>
    <div id="app">
        <nav>
            <router-link to="/">Home</router-link>
            <router-link to="/about">About</router-link>
        </nav>
        <router-view></router-view>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.js"></script>
    <script>
                // 定义组件:原本应该是在外面定义HomeComponent.vue [创建一些组件]
        const HomeComponent = {
            template: `<div><h1>Home</h1><p>Welcome to the Home page!</p></div>`
        };
              // 定义组件:原本应该是在外面定义AboutComponent.vue [创建一些组件]
        const AboutComponent = {
            template: `<div><h1>About</h1><p>Learn more about us on this page.</p></div>`
        };

        // 定义路由:原本应该是外面定义router.js文件来定义路由。
        const routes = [
            { path: '/', component: HomeComponent },
            { path: '/about', component: AboutComponent }
        ];

        // 创建路由实例
        const router = new VueRouter({
            routes
        });

        // 创建并挂载根实例
        const app = new Vue({
            router// 挂载路由
        }).$mount('#app');
    </script>
</body>
</html>

axios

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue with Axios Example</title>
</head>
<body>
    <div id="app">
        <h1>Data from API</h1>
        <ul>
            <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
        <!-- 使用 Vue 的 v-for 指令遍历 posts 数组,并生成列表项,每个列表项显示 post.title。 -->
        </ul>
    </div>

    <!-- 引入 Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <!-- 引入 Axios -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        // 创建 Vue 实例
        let vm = new Vue({
            el: "#app", // Vue 实例绑定到 HTML 中的 id="app" 的元素
            data: {
                posts: [] // 初始化一个空数组用于存储从 API 获取的数据
            },
            created() {
                // Vue 实例创建时调用的生命周期钩子
                axios.get('https://jsonplaceholder.typicode.com/posts') // 使用 Axios 发送 GET 请求
                    .then(response => { // 请求成功后的处理
                        this.posts = response.data; // 将响应数据赋值给 posts 数组
                    })
                    .catch(error => { // 请求失败后的处理
                        console.error('Error fetching data:', error); // 输出错误信息到控制台
                    });
            }
        });
    </script>
</body>
</html>

安装脚手架

CLI文档,开箱即用!
cmd全局安装:
npm install -g @vue/cli         或者
npm install -g @vue/cli-init

去想要的文件夹里面:C:\Users\Pluminary\Desktop\vue_cli>vue init webpack tuling
vue init webpack 项目名

? Project name tuling
? Project description A Vue.js project
? Author pcy
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No //严格检验Javascript语法的
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

完成后需要开启:
cd tuling
npm run dev

DONE  Compiled successfully in 1780ms 18:41:21
I  Your application is running here: http://localhost:8080

运用element-ui创建一个简易的登录界面

<template>
  <div class="hello-world">
    <h1>{{ message }}</h1>
    <el-button type="primary" @click="showAlert">Click Me</el-button>
  </div>

  <div class="app">
    <h3>{{passage}}</h3>
    <h4>{{getVal()}}</h4>
  </div>

   <!-- 使用Element UI的Table组件展示empList -->
  <el-table :data="empList" style="width: 100%">
    <el-table-column prop="name" label="Name" width="180"></el-table-column>
    <el-table-column prop="salary" label="Salary" width="180"></el-table-column>
  </el-table>

   <!-- 登录表单 -->
  <el-form ref="loginForm" :model="loginForm" class="loginForm" label-width="80px">
    <h2 class="loginTitle">人事后台管理系统</h2>
    <el-form-item label="用户名" prop="username">
      <el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
    </el-form-item>
    <el-form-item label="密码" prop="password">
      <el-input v-model="loginForm.password" type="password" placeholder="请输入密码"></el-input>
    </el-form-item>
    <el-form-item label="验证码" prop="code">
      <el-input v-model="loginForm.code" placeholder="请输入验证码"></el-input>
    </el-form-item>
    <el-form-item style="width:100%;">
      <el-button type="primary" @click="handleLogin">登录</el-button>
    </el-form-item>
  </el-form>
   

</template>

<script>
  export default {
    name: 'app',
    data(){
      return{
        passage: 'Oh?',
        message: 'Hello World!',
        empList:[
          { name: 'Peter', salary: '20000' },
          { name: 'Mike', salary: '16000' },
          { name: 'Tom', salary: '17000' }
        ],
        loginForm:{
          username:'',
          password:'',
          code:''
        }
      }
    },
    methods: {
      showAlert() {
        this.$message({
          message: 'Hello from Element UI!',
          type: 'success'
        });
      },
      getVal:function(){
        return "getVal的方法";
      },
      handleLogin() {
      this.$message({
        message: `登录成功,用户名: ${this.loginForm.username}`,
        type: 'success'
      });
     }
    }
  }

</script>

<style scoped>
    .hello-world {
      text-align: center;
      margin-top: 20px;
    }

    h1 {
      font-size: 24px;
      margin-bottom: 20px;
    }
    .app {
      margin-top: 30px;
    }
</style>
在Vue项目中,Element UI需要正确引入CSS样式和JavaScript文件才能正常工作。如果你没有一个HTML模板文件(例如index.html),而是在Vue组件中直接编写代码,你需要通过一些方式来确保Element UI的样式和脚本被正确加载。

// 1. 创建index.html文件:
这个index.html文件通常放在public文件夹下,是你Vue应用的入口文件。Vue CLI自动生成的项目通常会有这个文件。

// 2. 为什么需要这个HTML文件?
这个index.html文件是Vue CLI或手动设置的Vue项目的入口页面。当你运行npm run serve时,Vue CLI会将所有内容注入到<div id="app"></div>中,这个HTML文件是项目的基本框架。

Element UI的CSS和JS资源:需要在HTML文件的<head>中引入Element UI的样式文件(CSS)和在<body>中引入JS库,这样才能确保Element UI组件在你的Vue组件中正确渲染和运行。
    
// 3. 直接在Vue组件中引入:
如果你不想使用外部HTML文件,Vue项目中可以直接在main.js中引入Element UI的样式和组件:
/*
    // main.js
    import Vue from 'vue';
    import App from './App.vue';
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';

    Vue.config.productionTip = false;

    Vue.use(ElementUI);

    new Vue({
      render: h => h(App),
    }).$mount('#app');
*/
阅读全文

Cloud分布式微服务打造大型自媒体3大业务平台

2024/5/12

自媒体项目

VM CentOS7:数据库密码→Panchunyao123!
开发思维与企业一致
  • 三端融合:门户+媒体+运营
  • 4g自媒体辉煌时代
  • 5g科技互联网风口
  • 前后端分离式开发、代码动静分离、保证职
  • 能解耦、功能模块互相协调
如何整合分布式中间件到项目中
  • 整个流程会做到细致入微
  • 帮助迅速提升至少2年以上的项目经验
重点功能技术分析
  • Redis:分布式会话、session共享、单点登录、防刷、计数
  • Fastdfs+Nginx/OSS/GridFS:搭建分布式文件系统、单文件/批量上传、人脸隐私保护拦截
  • Maven:项目构建、聚合、分层、架构设计、面向对象
  • 阿里AI:人脸对比、文本/图片自动审核、短信
  • SpringCloud:业务分而治之、可伸缩、可扩展、接口服务化
  • Freemarker:构建模块页,实现页面静态化
前置技能必备
  • Java基础
  • 熟悉MySQL/MariaDB
  • 掌握Linux的基本命令
课程安排
  • 前端构建与运行
  • 后端手把手从0到1
  • 中间件手把手部署

前后端分离开发模式

传统JavaWeb开发 与 前后端页面交互

运行前端项目

Linux 安装 MySQL【CentOS】_linux 安装mysql-CSDN博客

前端代码在压缩包中 启动D:\apache-tomcat-8.5.93\bin\startup.bat
将里面的imooc-news放到D:\apache-tomcat-8.5.93\webapps中
去浏览器中启动 http://localhost:9090/imooc-news/portal/index.html
SwitchHosts

https://oldj.github.io/SwitchHosts/
在本地把域名和对应的IP給联系绑定起来 [相当于在云服务买了域名后绑定]

#imooc-news 127.0.0.1
[这东西需要关闭代理才可以用]

127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com

127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com

D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\js\app.js
window.app = {
    /* 
    portalIndexUrl: "http://localhost:8080/imooc-news/portal/index.html",           // 门户首页地址
    writerIndexUrl: "http://localhost:8080/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://localhost:8080/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    userServerUrl: "http://192.168.1.5:8003",   // 用户服务后端接口地址
    */

    portalIndexUrl: "http://www.imoocnews.com:9090/imooc-news/portal/index.html",           // 门户首页地址
    writerLoginUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/passport.html",      // 登录页面
    writerIndexUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    adminCenterUrl: "http://admin.imoocnews.com:9090/imooc-news/admin/contentReview.html",     // 运营管理平台主页

    userServerUrl: "http://user.imoocnews.com:8003",        // 用户服务后端接口地址
    fsServerUrl: "http://files.imoocnews.com:8004",         // 文件服务后端接口地址
    adminServerUrl: "http://admin.imoocnews.com:8005",      // 运营管理服务后端接口地址
    articleServerUrl: "http://article.imoocnews.com:8001",      // 文章服务后端接口地址

    /**
     * 如果本地使用localhost测试可以不使用,如果是ip或者域名测试,cookieDomain改为对应的ip或者域名
     * 例:
     *    ip:  192.168.1.111
     *    域名:   .imooc.com
     */
    cookieDomain: ".imoocnews.com",  
    ......
}

数据库选型与数据导入

  • MySql 5.6/5.7
  • MariaDB
  • Mysql 8.0
表名                             注释
admin_user                      运营管理平台的admin级别用户    
app_user                      网站用户
article                          文章资讯表
category                      新闻资讯文章的分类(或称之为领域)
comments                      评论表
fans                          粉丝表,用户与粉丝的关联关系,粉丝本质也是用户

构建Maven聚合工程

创建一个 imooc-news-dev 的Maven项目作为一个顶级工程项目

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
<!--
    1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
      这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
      模块与模块之间是平级的,是可以相互依赖的
    2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
      必须构建依赖(构建关系)
    3.一个顶级工程是可以由多个不同的子工程共同组合而成
-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,父工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>
            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
imooc-news-dev-common
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
imooc-news-dev-model
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-model
    模型工程, 所有的子工程以及微服务中所涉及到的模型实体类都在此管理
    可以包含一些 *pojo,*Bean,*Entity,vo,bo,dto等
-->

    <artifactId>imooc-news-dev-model</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
<!--  子工程依赖common -->
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-api
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!--    imooc-news-dev-service-api
            接口工程,集中管理所有的controller中的接口,为了更好的统一管理微服务
    -->
    <artifactId>imooc-news-dev-service-api</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-user
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-user</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
    </dependencies>
</project>
imooc-news-dev [父]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
    <!-- 这里是子模块[自动创建] 如果物理删除了子工程 这个不会自动删除的要手动删除-->
        <module>imooc-news-dev-common</module>
        <module>imooc-news-dev-model</module>
        <module>imooc-news-dev-service-api</module>
        <module>imooc-news-dev-service-user</module>
        <module>imooc-news-dev-service-user</module>
    </modules>
    <!--
        1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
          这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
          模块与模块之间是平级的,是可以相互依赖的
        2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
          必须构建依赖(构建关系)
        3.一个顶级工程是可以由多个不同的子工程共同组合而成
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,夫工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>

            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Object hello(){
        return "hello";
    }
}
com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
  port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
SwitchHosts【一定要先关闭代理 因为它会固定静态ip】
127.0.0.1 user.imoocnews.com

http://user.imoocnews.com:8003/hello

api接口暴露

所有业务下不同的controller都要交给api统一接口去处理,把实现所对应的接口写进API内
把imooc-news-dev-service-user中的pom.xml中的关于SpringBoot的依赖
全部放入imooc-news-dev-service-api中
并且在imooc-news-dev-service-user中写入引用依赖
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
【服务层依赖api层】
更改把user项目的HelloController 复制到dev-service-api 创建一个接口

...api
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;
import org.springframework.web.bind.annotation.GetMapping;

public interface HelloControllerApi {
    /**
     * api的作用:
     * api就相当于企业的领导,老板,部门经理
     * 其他的服务层都是实现,他们就相当于员工,只做事情
     * 老板(开发人员)来看一下每个人(服务)的进度,做什么事
     * 老板不会去问员工,他只会对接部门经理
     * 这里所有的api接口就是统一在这里管理和调度的,微服务也如此
     */

    /**
     * 运作:
     * 现在的所有接口都在此暴露,实现都是在各自的微服务中
     * 本项目只写项目,不写实现,实现在各自的微服务工程中,因为以业务来划分的微服务有很多
     * Controller也会分散在各个微服务工程中,一旦多了就很难统一管理和查看
     *
     * 其次,微服务之间的调用都是基于接口的
     * 如果不这样做,微服务之间的调用就需要互相依赖了
     * 耦合对也就很高,接口的目的是为了能够提供解耦
     *
     * 此外,本项目的接口其实就是一套规范.实现都是由各自的工程去做的处理
     * 目前我们使用springboot作为接口的实现的
     * 如果未来以后出现新的java web框架,那么我们不需要修改接口
     * 只需要去修改对应的实现就可以了,这其实也是解耦的一个体现
     *
     * Swagger2, 基于接口的自动文档生成
     * 所有的配置文件只需要一份,就能再当前项目中去构建了
     * 管理起来很方便
     * 
     * 综上所述,这样做法可以提高多服务的项目可扩展性
     */
    @GetMapping("/hello")
    public Object hello();
}


...user
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    public Object hello(){
        return "hello";
    }
}

配置logback日志与多环境profile

imooc-news-dev-service-user

先添加一个logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-dev"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-user.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

        return "hello";
    }
}
//重新启动后去页面刷新一下
......
29:22.373 [main] INFO  com.imooc.user.Application - Started Application in 1.061 seconds (JVM running for 1.543)
29:40.693 [http-nio-8003-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
29:40.694 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
29:40.696 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
29:40.707 [http-nio-8003-exec-1] INFO  c.i.user.controller.HelloController - info: hello~
29:40.707 [http-nio-8003-exec-1] WARN  c.i.user.controller.HelloController - warn: hello~
29:40.707 [http-nio-8003-exec-1] ERROR c.i.user.controller.HelloController - error: hello~
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
application-dev.yml
server:
  port: 8003


application-prod.yml
server:
  port: 8130

优雅的返回封装结果

com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.IMOOCJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
        return IMOOCJSONResult.errorMsg("您的信息有误");
    }
}
com/imooc/grace/result/IMOOCJSONResult.java
package com.imooc.grace.result;

/**
 * 
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 * 
 *                 200:表示成功
 *                 500:表示错误,错误信息在msg字段中
 *                 501:bean验证错误,不管多少个错误都以map形式返回
 *                 502:拦截器拦截到用户token出错
 *                 555:异常抛出信息
 *                 556: 用户qq校验异常
 *                 557: 校验用户是否在CAS登录,用户门票的校验
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V1.0
 * 这样太麻烦了 直接用枚举类
 */
public class IMOOCJSONResult {

    // 响应业务状态
    private Integer status;

    // 响应消息
    private String msg;

    // 响应中的数据
    private Object data;
    
    private String ok;    // 不使用

    public static IMOOCJSONResult build(Integer status, String msg, Object data) {
        return new IMOOCJSONResult(status, msg, data);
    }

    public static IMOOCJSONResult build(Integer status, String msg, Object data, String ok) {
        return new IMOOCJSONResult(status, msg, data, ok);
    }
    
    public static IMOOCJSONResult ok(Object data) {
        return new IMOOCJSONResult(data);
    }

    public static IMOOCJSONResult ok() {
        return new IMOOCJSONResult(null);
    }
    
    public static IMOOCJSONResult errorMsg(String msg) {
        return new IMOOCJSONResult(500, msg, null);
    }

    public static IMOOCJSONResult errorUserTicket(String msg) {
        return new IMOOCJSONResult(557, msg, null);
    }
    
    public static IMOOCJSONResult errorMap(Object data) {
        return new IMOOCJSONResult(501, "error", data);
    }
    
    public static IMOOCJSONResult errorTokenMsg(String msg) {
        return new IMOOCJSONResult(502, msg, null);
    }
    
    public static IMOOCJSONResult errorException(String msg) {
        return new IMOOCJSONResult(555, msg, null);
    }
    
    public static IMOOCJSONResult errorUserQQ(String msg) {
        return new IMOOCJSONResult(556, msg, null);
    }

    public IMOOCJSONResult() {

    }

    public IMOOCJSONResult(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
    
    public IMOOCJSONResult(Integer status, String msg, Object data, String ok) {
        this.status = status;
        this.msg = msg;
        this.data = data;
        this.ok = ok;
    }

    public IMOOCJSONResult(Object data) {
        this.status = 200;
        this.msg = "OK";
        this.data = data;
    } Getter+Setter
这样太麻烦而且观察起来不方便 升级一下变成枚举类 更加优雅!
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.IMOOCJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
//        return IMOOCJSONResult.errorMsg("您的信息有误");
        return GraceJSONResult.errorCustom(ResponseStatusEnum.NO_AUTH);
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }Getter+Setter
}
com/imooc/grace/result/ResponseStatusEnum.java
package com.imooc.grace.result;

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_ERROR(513,false,"仅支持500kb大小以下的图片上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员登录名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空后者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_NOT_EXIT_ERROR(567, false, "管理员不存在或密码错误!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, "");


    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}

配置数据库逆向生成实体类

引入mybatis-generator-database[新建工程项目]
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>mybatis-generator-database</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>

        <!-- 引入log4j日志依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>

        <!-- 阿里开源数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.41</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>1.2.4</version>
        </dependency>
        <!--pagehelper-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- mybatis 逆向生成工具  -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

整合Mybatis

dependencymanagement 无法引入包依赖-CSDN博客
关于Maven依赖dependency无法引入的问题_ 无法引入包-CSDN博客
Maven配置仓库、阿里云镜像、环境变量(史上最全最详细)_maven配置阿里云镜像-CSDN博客

将mybatis-generator-database中的pojo[AppUser+Fans]复制转移到imooc-news-dev-model com/imooc/pojo中
将mybatis-generator-database中的com/imooc/user/mapper[AppUserMapper、FansMapper]复制转移到imooc-news-service-user com/imooc/user/mapper中
所有的服务都是要实现API的
将mybatis-generator-database中的com/imooc/my/mapper[MyMapper]复制转移到imooc-news-dev-service-api com/imooc/my/mapper中
将..database中的resources的mapper[AppUserMapper.xml、FansMapper.xml]转移到...service-user的resources的mapper中
imooc-news-dev-service-user中的resources的application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: com.mysql.jdbc.Driver          # mysql驱动
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    username: root
    password: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.imooc.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.imooc.my.mapper.MyMapper
  not-empty: false    # 在进行数据库操作的的时候,判断表达式 username != null, 是否追加 username != ''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true

【解决】SLF4J: Class path contains multiple SLF4J bindings._启动metastore时slf4j: class path contains multiple sl-CSDN博客
整合MongoDB踩坑记录及解决方法 - 会飞的猪仔 - 博客园 (cnblogs.com)

com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan("com.imooc")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Swagger2接口文檔工具的使用

imooc-news-dev-service-api
<!-- swagger2 配置 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>${swagger-bootstrap-ui.version}</version>
        </dependency>
com/imooc/api/config/Swagger2.java
package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
//http://user.imoocnews.com:8003/swagger-ui.htm
    //    http://localhost:8088/doc.html            新路径
//http://user.imoocnews.com:8003/doc.html

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
//        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
//                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "controller的標題",tags = {"xx功能的Controller"})
public interface HelloControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "GET")
    @GetMapping("/hello")
    public Object hello();
}

[maven-之Lifecycle详解_maven lifecycle-CSDN博客](https://blog.csdn.net/qq_39505065/article/details/102915403#第三部分 PS)

Maven中的Lifecycle的install是什麽 ?
将包安装到本地存储库中,作为本地其他项目的依赖项

梳理短信登錄注冊流程

  • 短信登录注册
  • 短信验证码发送与限制
  • 分布式会话
  • 用户信息完善,OSS/FastDFS文件上传
  • AOP日志监控

aliyun.properties
#这里需要去阿里云上购买短信验证 [电脑aliyun_Key.txt有]
aliyun.accessKeyID=
aliyun.accessKeySecret=

SendSms_短信服务_API调试-阿里云OpenAPI开发者门户 (aliyun.com)
短信发送并查询示例_短信服务_示例中心-阿里云OpenAPI开发者门户 (aliyun.com)
RAM 访问控制 (aliyun.com)
短信服务 (aliyun.com)
云服务器管理控制台 (aliyun.com)

【imooc-news-dev-common】
pom.xml
加入springboot依赖包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <!--  第三方云厂商相关依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
com/imooc/utils/extend/AliyunResource.java
package com.imooc.utils.extend;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun") //这里是前缀
public class AliyunResource {
    private String accessKeyID;
    private String accessKeySecret;

    public String getAccessKeyID() {
        return accessKeyID;
    }

    public void setAccessKeyID(String accessKeyID) {
        this.accessKeyID = accessKeyID;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }
}
com/imooc/utils/SMSUtils.java
package com.imooc.utils;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.imooc.utils.extend.AliyunResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component //工具类 可以作为组件
public class SMSUtils {
    @Autowired
    public AliyunResource aliyunResource;
    final static Logger logger = LoggerFactory.getLogger(SMSUtils.class);
    public void sendSMS(String mobile, String code) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
                aliyunResource.getAccessKeyID(),
                aliyunResource.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", "cn-hangzhou");
        //给对方发送的手机号
        request.putQueryParameter("PhoneNumbers", mobile);
        request.putQueryParameter("SignName", "小潘科技");//控制台可以添加签名
        request.putQueryParameter("TemplateCode", "SMS_467115116");
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//JSON对象字符串
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            // 打印阿里云API的响应结果
            logger.info("Aliyun SMS API response: " + response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
            logger.error("ServerException: " + e.getMessage());
        } catch (ClientException e) {
            e.printStackTrace();
            logger.error("ClientException: " + e.getMessage());
        }
    }
}
com/imooc/api/controller/user/PassportControllerApi.java[接口]
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode();
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("passport")
public class PassportController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(){
        // 生成6位随机验证码
        String random = String.valueOf((int)((Math.random() * 9 + 1) * 100000));
        // 打印生成的验证码以便调试
        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
        logger.info("SMS sent to 15027597000 with code: " + random);

        return GraceJSONResult.ok();
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }

    public GraceJSONResult() {
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }
}

安装配置整合Redis

通过Xftp7把redis-5.0.7.tar.gz传入到服务器
[root@iZbp1dssknxftmjczbtpndZ ~]# tar -zxvf redis-5.0.7.tar.gz 
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
apache-zookeeper-3.6.0-bin         rabbitmq-server-3.8.2-1.el7.noarch.rpm
apache-zookeeper-3.6.0-bin.tar.gz  redis-5.0.7
erlang-22.3-1.el7.x86_64.rpm       redis-5.0.7.tar.gz
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ll
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# yum install gcc-c++
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# make
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/ ★
[root@iZbp1dssknxftmjczbtpndZ local]# ll
[root@iZbp1dssknxftmjczbtpndZ local]# pwd
/usr/local
[root@iZbp1dssknxftmjczbtpndZ local]# cd redis/ ★
[root@iZbp1dssknxftmjczbtpndZ redis]# ll
total 4
drwxr-xr-x 2 root root 4096 May 15 11:19 bin
[root@iZbp1dssknxftmjczbtpndZ redis]# cd bin ★
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32772
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root       0 May 15 11:19 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ redis]# cd
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ls
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cp redis.conf /usr/local/redis/bin/
cp: overwrite ‘/usr/local/redis/bin/redis.conf’? y
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/redis/bin/
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32836
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root   61797 May 15 11:28 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ bin]# vim redis.conf
在里面 /bind 直接搜索
把bind 127.0.0.1修改成 0.0.0.0在任何地方都可以进行操作修改
在里面 /dae
把daemonize no 改成 daemonize yes[后台启动]
在里面 /require
把requirepass foobared 这里是设置密码 requirepass XXXX
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-server redis.conf ★
32421:C 15 May 2024 11:35:36.687 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
32421:C 15 May 2024 11:35:36.687 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=32421, just started
32421:C 15 May 2024 11:35:36.687 # Configuration loaded
[root@iZbp1dssknxftmjczbtpndZ bin]# ps -ef|grep redis
root     32422     1  0 11:35 ?        00:00:00 ./redis-server 0.0.0.0:6379
root     32456 25226  0 11:35 pts/0    00:00:00 grep --color=auto redis
【此时说明已经成功启动Redis】
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli ★
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name imooc
OK
127.0.0.1:6379> get name
"imooc"
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli -p 6379 shutdown

★直接进入redis文件内★
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/local/redis/bin       //进入文件内
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-server redis.conf   //启动
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli                 //测试
★★

Linux下Redis服务启动与关闭_linux 关闭redis-CSDN博客

安装配置整合Redis-2

下载并安装好 Redis Desktop Manager
新连接设置
名字:redis-imooc-news 47.98.225.105
地址:47.98.225.105:6379

加一下redis的依赖
imooc-news-dev-common
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- 引入 redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>
        <!-- jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- apache 工具类 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </dependency>
        <!-- google 工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <!-- joda-time 时间工具 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

        <!--  第三方云厂商相关依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
com/imooc/utils/RedisOperator.java
package com.imooc.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Title: Redis 工具类
 * @author 风间影月
 */
@Component
public class RedisOperator {
    
    @Resource
    private StringRedisTemplate redisTemplate;

    // Key(键),简单的key-value操作

    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public boolean keyIsExist(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     * 
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }
    
    /**
     * 实现命令:expire 设置过期时间,单位秒
     * 
     * @param key
     * @return
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 实现命令:increment key,增加key一次
     * 
     * @param key
     * @return
     */
    public long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 实现命令:decrement key,减少key一次
     *
     * @param key
     * @return
     */
    public long decrement(String key, long delta) {
        return redisTemplate.opsForValue().decrement(key, delta);
    }

    /**
     * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 实现命令:DEL key,删除一个key
     * 
     * @param key
     */
    public void del(String key) {
        redisTemplate.delete(key);
    }

    // String(字符串)

    /**
     * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
     * 
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
     * 
     * @param key
     * @param value
     * @param timeout
     *            (以秒为单位)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     * @param key
     * @param value
     */
    public void setnx60s(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     * @param key
     * @param value
     */
    public void setnx(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 实现命令:GET key,返回 key所关联的字符串值。
     * 
     * @param key
     * @return value
     */
    public String get(String key) {
        return (String)redisTemplate.opsForValue().get(key);
    }

    /**
     * 批量查询,对应mget
     * @param keys
     * @return
     */
    public List<String> mget(List<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 批量查询,管道pipeline
     * @param keys
     * @return
     */
    public List<Object> batchGet(List<String> keys) {

//        nginx -> keepalive
//        redis -> pipeline

        List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                StringRedisConnection src = (StringRedisConnection)connection;

                for (String k : keys) {
                    src.get(k);
                }
                return null;
            }
        });

        return result;
    }


    // Hash(哈希表)

    /**
     * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
     * 
     * @param key
     * @param field
     * @param value
     */
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
     * 
     * @param key
     * @param field
     * @return
     */
    public String hget(String key, String field) {
        return (String) redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
     * 
     * @param key
     * @param fields
     */
    public void hdel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
     * 
     * @param key
     * @return
     */
    public Map<Object, Object> hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List(列表)

    /**
     * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
     * 
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long lpush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 实现命令:LPOP key,移除并返回列表 key的头元素。
     * 
     * @param key
     * @return 列表key的头元素。
     */
    public String lpop(String key) {
        return (String)redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
     * 
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }
}

Spring Boot集成Redis的坑,踩了! - 知乎 (zhihu.com)
@Autowired和@Resource注解的区别和联系(十分详细,不看后悔)_为什么@resource和@autowired 注入的对象不一样-CSDN博客

妈的有个超级大bug 整我一下午,
@Component public class RedisOperator {
@Autowired private StringRedisTemplate redisTemplate;}
报错信息 Could not autowire. No beans of ‘StringRedisTemplate’ type found.

在这里不要本末倒置 回归最原始的报错 那就是pom.xml中的导包依赖问题
有的时候直接复制的项目中的成熟依赖 根据时代的不同可能会导致丢失无法下载依赖
这时要去百度Maven库手动下载 并且手动添加 然后手动导入Project Structure → Libraries 手动导入自己需要的包[记住包的版本 也要在依赖里面体现 < version >],而且如果有红色波浪线的包可以删除 再重新导入即可

Spring里遇到的一个问题,autowired时报找不到bean定义_autowired找不到bean-CSDN博客

完善发送短信接口

com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpServletRequest;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request);
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.controller.user.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.imooc.utils.IPUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
// 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
//    @Autowired
//    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        String userIp = IPUtil.getRequestIp(request);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
//        redis.setnx60s("smscode"+ip);
        redis.setnx60s(MOBILE_SMSCODE + ":"+userIp,userIp);

        //生成随机验证码并且发送短信
        String random = ((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替

        //把验证码存入redis,用于后续进行验证
        redis.set(MOBILE_SMSCODE+":"+mobile, random, 30*60);
        return GraceJSONResult.ok();
    }
}
com/imooc/api/controller/user/BaseController.java
package com.imooc.api.controller.user;

import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;

public class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";

}
com/imooc/utils/IPUtil.java
package com.imooc.utils;

import javax.servlet.http.HttpServletRequest;

/**
 * 用户获得用户ip的工具类
 */
public class IPUtil {

    /**
     * 获取请求IP:
     * 用户的真实IP不能使用request.getRemoteAddr()
     * 这是因为可能会使用一些代理软件,这样ip获取就不准确了
     * 此外我们如果使用了多级(LVS/Nginx)反向代理的话,ip需要从X-Forwarded-For中获得第一个非unknown的IP才是用户的有效ip。
     * @param request
     * @return
     */
    public static String getRequestIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

快速使用 在控制台发送短信_短信服务(SMS)-阿里云帮助中心 (aliyun.com)

联调前端发送短信, 解决跨域问题

http://writer.imoocnews.com:9090/imooc-news/writer/passport.html
因为后台写死了手机号 所以在输入手机号可以随便 点击发送验证码后 会在浏览器控制台输出跨域问题 在后端要設置允許跨域請求
-----------------------------------------------------------------------------------
passport.html:1  Access to XMLHttpRequest at 'http://user.imoocnews.com:8003/passport/getSMSCode?mobile=123334323' from origin 'http://writer.imoocnews.com:9090' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

GET http://user.imoocnews.com:8003/passport/getSMSCode?mobile=123334323 net::ERR_FAILED 200 (OK)
axios.min.js:2  Uncaught (in promise) Error: Network Error
    at e.exports (axios.min.js:2:9633)
    at l.onerror (axios.min.js:2:8398)
...service-api  com/imooc/api/config/CorsConfig.java
package com.imooc.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration //SpringBoot可以加载该信息
public class CorsConfig {

    public CorsConfig() {
    }

    @Bean
    public CorsFilter corsFilter() {
        // 1. 添加cors配置信息
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        // 设置是否发送cookie信息
        config.setAllowCredentials(true);
        // 设置允许请求的方式
        config.addAllowedMethod("*");
        // 设置允许的header
        config.addAllowedHeader("*");
        // 2. 为url添加映射路径
        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/**", config);
        // 3. 返回重新定义好的corsSource
        return new CorsFilter(corsSource);
    }
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
   //记得如果要发送到redis中 则需要先用application-dev.yml导入RedisDesktopManager正确的网络地址127.0.0.1 
        return GraceJSONResult.ok();
    }
}
com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);
}

拦截并限制60秒用户短信发送

service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/password/getSMSCode"); //拦截PassportControllerApi里的信息
    }
}
com/imooc/api/interceptors/PassportInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

    public class PassportInterceptor implements HandlerInterceptor {

        @Autowired
        public RedisOperator redis;

        public static final String MOBILE_SMSCODE = "mobile:smscode";

        /**
         * 拦截请求,访问controller之前
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

            // 获得用户ip
            String userIp = IPUtil.getRequestIp(request);

            boolean keyIsExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);

            if (keyIsExist) {
                GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);
//            System.out.println("短信发送频率太大!");
                return false;
            }

            /**
             * false:请求被拦截
             * true:请求通过验证,放行
             */
            return true;
        }


        /**
         * 请求访问到controller之后,渲染视图之前
         * @param request
         * @param response
         * @param handler
         * @param modelAndView
         * @throws Exception
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        }

        /**
         * 请求访问到controller之后,渲染视图之后
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        }
    }
dev-common com/imooc/exception/GraceException.java
package com.imooc.exception;

import com.imooc.grace.result.ResponseStatusEnum;

/**
 * 优雅的处理异常,统一封装
 */
public class GraceException {

    public static void display(ResponseStatusEnum responseStatusEnum) {
        throw new MyCustomException(responseStatusEnum);
    }

}
com/imooc/exception/MyCustomException.java
package com.imooc.exception;

import com.imooc.grace.result.ResponseStatusEnum;

/**
 * 自定义异常
 * 目的:统一处理异常信息
 *      便于解耦,service与controller错误的解耦,不会被service返回的类型而限制
 */
public class MyCustomException extends RuntimeException {

    private ResponseStatusEnum responseStatusEnum;

    public MyCustomException(ResponseStatusEnum responseStatusEnum) {
        super("异常状态码为:" + responseStatusEnum.status()
                + ";具体异常信息为:" + responseStatusEnum.msg());
        this.responseStatusEnum = responseStatusEnum;
    }

    public ResponseStatusEnum getResponseStatusEnum() {
        return responseStatusEnum;
    }

    public void setResponseStatusEnum(ResponseStatusEnum responseStatusEnum) {
        this.responseStatusEnum = responseStatusEnum;
    }
}

自定义异常, 返回错误信息

[接上方GraceException、MyCustomException、PassportInterceptor]
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
package com.imooc.grace.result;

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_ERROR(513,false,"仅支持500kb大小以下的图片上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员登录名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空后者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_NOT_EXIT_ERROR(567, false, "管理员不存在或密码错误!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, "");


    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}
dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice //本质上是实现AOP的管理
public class GraceExceptionHandler {
    @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }
}

验证BO信息(注册登录接口)

service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO, BindingResult result);
}
dev-model  com/imooc/pojo/bo/RegistLoginBO.java
package com.imooc.pojo.bo;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

//加上@data 会自动生成getter+setter
public class RegistLoginBO {
    //不为空 空的话可以返回 不用NOTNULL因为无法校验空字符串 用NotBlank
    @NotBlank(message = "手机号不能为空") 
    private String mobile;
    @NotBlank(message = "短信验证码不能为空")
    private String smsCode;

    @Override
    public String toString() {
        return "RegistLoginBO{" +
                "mobile='" + mobile + '\'' +
                ", smsCode='" + smsCode + '\'' +
                '}';
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getSmsCode() {
        return smsCode;
    }

    public void setSmsCode(String smsCode) {
        this.smsCode = smsCode;
    }
}
service-user  com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }
        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/BaseController.java
package com.imooc.api;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";

    /**可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result){
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList){
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }
}
http://writer.imoocnews.com:8003/doc.html   打开校验
POST:/passport/doLogin

{
    "mobile":"",
    "smsCode":""
}

{
"status": 500,
"msg": "操作失败!",
"success": false,
-"data": {
"smsCode": "短信验证码不能为空",
"mobile": "手机号不能为空"
}
}
--------------------------------------------------------
 //不为空 空的话可以返回 不用NOTNULL因为无法校验空字符串 用NotBlank
    @NotBlank(message = "手机号不能为空") 
    private String mobile;
    @NotBlank(message = "短信验证码不能为空")
    private String smsCode;
// 要注意上面的为NotBlank 不然它验证的结果会跳过手机号判断 直接说验证码错误 
// 因为NotNull在处理"mobile":"", "smsCode":""的时候空字符串也算入不为空
//NotBlank兼顾NotNull

通过数据库 查询老用户_老用户添加

service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO, BindingResult result);
}
service-user  com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.enums.UserStatus;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;

    @Autowired
    private UserService userService;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }
        return GraceJSONResult.ok(user);
    }
}
service-user  com/imooc/user/service/impl/UserService.java[接口]
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);
}
service-user  com/imooc/user/service/UserServiceimpl.java
package com.imooc.user.service;

import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.DesensitizationUtil;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import com.imooc.utils.DateUtil;

import java.util.Date;

@Service
public class UserServiceimpl implements UserService {
    @Autowired
    public AppUserMapper appUserMapper; //基本的CRUD都可以

    @Autowired
    public Sid sid;

    private static final String USER_FACE0 = "https://raw.githubusercontent.com/P-luminary/images/10d94134b65e13cc8ec9b8a9aeae4f958921cab7/data/Imooc_Cat.jpg";
    private static final String USER_FACE1 = "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg";
    @Override
    public AppUser queryMobileIsExist(String mobile) {
        Example userExample = new Example(AppUser.class);
        Example.Criteria userCriteria = userExample.createCriteria();
        userCriteria.andEqualTo("mobile", mobile);
        AppUser user = appUserMapper.selectOneByExample(userExample);
        return null;
    }

    @Transactional //对整个类的方法,事务起作用。无异常时正常提交,有异常时数据回滚
    @Override
    public AppUser createUser(String mobile) {
        /**
         * 互联网项目都要考虑可扩展性
         * 如果未来的业务激增,那么就需要分表分库
         * 那么数据库表主键id必须保证全局(全库)唯一,不得重复
         */
        String userId = sid.nextShort();
        AppUser user = new AppUser();
        user.setId(userId);
        user.setMobile(mobile);
        user.setNickname("用户:" + DesensitizationUtil.commonDisplay(mobile)); //給手机号加** 是脱敏操作
        user.setFace(USER_FACE1);
        user.setBirthday(DateUtil.stringToDate("2024-06-29")); //字符串转换Date类型
        user.setSex(Sex.secret.type);
        user.setActiveStatus(UserStatus.INACTIVE.type);//是否激活
        user.setTotalIncome(0);//收入
        user.setCreatedTime(new Date());
        user.setUpdatedTime(new Date());
        return user;
    }
}
dev-model  com/imooc/pojo/AppUser.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Table(name = "app_user")
public class AppUser {
    @Id
    private String id;

    /**
     * 手机号
     */
    private String mobile;

    /**
     * 昵称,媒体号
     */
    private String nickname;

    /**
     * 头像
     */
    private String face;

    /**
     * 真实姓名
     */
    private String realname;

    /**
     * 邮箱地址
     */
    private String email;

    /**
     * 性别 1:男  0:女  2:保密
     */
    private Integer sex;

    /**
     * 生日
     */
    private Date birthday;

    /**
     * 省份
     */
    private String province;

    /**
     * 城市
     */
    private String city;

    /**
     * 区县
     */
    private String district;

    /**
     * 用户状态:0:未激活。 1:已激活:基本信息是否完善,真实姓名,邮箱地址,性别,生日,住址等,如果没有完善,则用户不能发表评论,不能点赞,不能关注。2:已冻结。
     */
    @Column(name = "active_status")
    private Integer activeStatus;

    /**
     * 累计已结算的收入金额,也就是已经打款的金额,每次打款后再此累加
     */
    @Column(name = "total_income")
    private Integer totalIncome;

    /**
     * 创建时间 创建时间
     */
    @Column(name = "created_time")
    private Date createdTime;

    /**
     * 更新时间 更新时间
     */
    @Column(name = "updated_time")
    private Date updatedTime;
}Getter + Setter
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","com.imooc.utils"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
【来自Chatgpt】
java.sql.SQLException: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'READ' at line 1
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.readErrorPacket(AbstractQueryProtocol.java:1594)
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.readPacket(AbstractQueryProtocol.java:1453)
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.getResult(AbstractQueryProtocol.java:1415)

这个新的错误信息表明在设置 tx_isolation 时,语法存在问题。我们需要确保设置事务隔离级别的语法是正确的。以下是一些更具体的步骤来解决这个问题:

1. 确认MariaDB版本支持的语法
在MariaDB中,设置事务隔离级别的正确语法可能与MySQL略有不同。确保使用MariaDB支持的正确语法。

2. 确认正确的事务隔离级别语法
根据MariaDB的官方文档,设置事务隔离级别的正确方法是使用以下语法:

sql
复制代码
SET SESSION tx_isolation = 'READ-COMMITTED';
3. 修改连接URL
在连接URL中正确设置事务隔离级别:

yaml
复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&sessionVariables=tx_isolation='READ-COMMITTED'
4. 确认数据源属性配置
确保数据源属性配置正确:

yaml
复制代码
spring:
  datasource:
    hikari:
      connection-timeout: 30000
      minimum-idle: 5
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 600000
      pool-name: DateSourceHikariCP
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
5. 确认使用MariaDB JDBC驱动
确保使用MariaDB的JDBC驱动:

Maven
xml
复制代码
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>2.7.2</version>
</dependency>
Gradle
groovy
复制代码
implementation 'org.mariadb.jdbc:mariadb-java-client:2.7.2'
6. 测试事务隔离设置
在MariaDB中手动测试设置事务隔离级别,确保语法正确:

sql
复制代码
SET SESSION tx_isolation = 'READ-COMMITTED';
如果这条命令可以成功执行,那么它也应该能在你的连接URL中正确设置。

7. 确认Spring配置
确保Spring Boot配置正确,可以通过显式设置数据源属性来避免问题:

yaml
复制代码
spring:
  datasource:
    hikari:
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
总结
通过上述步骤,你应该能够解决 tx_isolation 变量设置的问题,并确保连接到MariaDB数据库时没有语法错误。如果问题依然存在,请提供更多的错误日志和配置文件信息,以便进一步诊断。
http://writer.imoocnews.com:8003/doc.html 先接收验证码 通过redis获得 再测试登录接口
{
    "mobile":"15027597319",
    "smsCode":"815850"
}

相应内容:
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    -"data": {
    "id": "240629F0PD4PZANC",
    "mobile": "15027597319",
    "nickname": "用户:15******319",
    "face": "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg",
    "realname": null,
    "email": null,
    "sex": 2,
    "birthday": "2024-06-29 00:00:00",
    "province": null,
    "city": null,
    "district": null,
    "activeStatus": 0,
    "totalIncome": 0,
    "createdTime": "2024-06-29 19:39:10",
    "updatedTime": "2024-06-29 19:39:10"
    }
}

此时去数据库imooc-news-dev的app_user中发现并未有数据新增进入
再UserServiceimpl.java中
appUserMapper.insert(user);

当如果把app_user数据库离的active_status 的0变成2 就会被冻结【UserStatus】
{
"status": 507,
"msg": "用户已被冻结,请联系管理员!",
"success": false,
"data": null
}

设置会话与cookie信息【注册登录】

service-user  com/imooc/user/controller/PassportController.java
...
 // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis
            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi

        }
        return GraceJSONResult.ok(user);
    }
...
service-api  com/imooc/api/BaseController.java [增加一个setCookie]
package com.imooc.api;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;

    /**可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result){
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList){
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }

/*    public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            Cookie cookie = new Cookie(cookieName,cookieValue);
            cookie.setMaxAge(maxAge);
            cookie.setDomain("imoocnews.com");
            cookie.setPath("/");//都用cookie
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    } */

   public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
            Cookie cookie = new Cookie(cookieName,cookieValue);
            cookie.setMaxAge(maxAge);
            cookie.setDomain("imoocnews.com");
            cookie.setPath("/");//都用cookie
            response.addCookie(cookie);//把cookie传入
        }
}
service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO
            , BindingResult result, HttpServletRequest request, HttpServletResponse response);
    //完成之后 去BaseController里面写一个setCookie()方便都可以用
}
service-user  com/imooc/user/controller/PassportController.java
    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result, HttpServletRequest request, HttpServletResponse response) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }

        // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis

            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi
            setCookie(request, response,"uToken",uToken,COOKIE_MONTH);
            setCookie(request, response,"uid",user.getId(),COOKIE_MONTH);
        }
        // 4.用户登录或注册成功以后,需要删除redis中的短信验证码,验证码只能使用一次,用过则作废
        redis.del(MOBILE_SMSCODE + ":" + mobile);
        // 5.返回用户状态 返回前端看
        return GraceJSONResult.ok(userActiveStatus);
    }

资源属性与常量绑定 [优雅]

把这种属性放到常量文件里进行绑定  cookie.setDomain("imoocnews.com");
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
 ★ @Value("${website.domain-name}") ★★
 ★ public String DOMAIN_NAME; ★★
...
 public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }
...
================================================================================
application-dev.yml
server:
  port: 8003

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

查询用户账户信息

service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);
}
service-user  com/imooc/user/controller/UserController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.vo.UserAccountInfoVO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class UserController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getAccountInfo(String userId) {
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }

        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        UserAccountInfoVO accountInfoVO = new UserAccountInfoVO();
        BeanUtils.copyProperties(user, accountInfoVO); //拷贝信息
        return GraceJSONResult.ok(accountInfoVO);
    }
    private AppUser getUser(String userId){
        // TODO 本方法后续公用,并且扩展
        AppUser user = userService.getUser(userId);
        return user;
    }
}
service-user  com/imooc/user/service/impl/UserService.java
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);

    /**
     * 根据用户主键id查询用户信息
     * @param userId
     * @return
     */
    public AppUser getUser(String userId);
}
====================================================================
service-user  com/imooc/user/service/UserServiceimpl.java
     @Override
    public AppUser getUser(String userId) {
        return appUserMapper.selectByPrimaryKey(userId);
    }
dev-model  com/imooc/pojo/vo/UserAccountInfoVO.java
public class UserAccountInfoVO {
    private String id;
    private String mobile;
    private String nickname;
    private String face;
    private String realname;
    private String email;
    private Integer sex;
    private Date birthday;
    private String province;
    private String city;
    private String district;
}Getter + Setter

信息校验

service-user  com/imooc/user/controller/UserController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.pojo.vo.UserAccountInfoVO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@RestController
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getAccountInfo(String userId) {
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }

        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        UserAccountInfoVO accountInfoVO = new UserAccountInfoVO();
        BeanUtils.copyProperties(user, accountInfoVO); //拷贝信息
        return GraceJSONResult.ok(accountInfoVO);
    }

    private AppUser getUser(String userId){
        // TODO 本方法后续公用,并且扩展
        AppUser user = userService.getUser(userId);
        return user;
    }

    @Override
    public GraceJSONResult updateUserInfo(UpdateUserInfoBO updateUserInfoBO, BindingResult result) {
        // 0.校验BO
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        // 1.执行更新操作
        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          BindingResult result);
}
dev-model  com/imooc/pojo/bo/UpdateUserInfoBO.java
public class UpdateUserInfoBO {

    @NotBlank(message = "用户ID不能为空")
    private String id;

    @NotBlank(message = "用户昵称不能为空")
    @Length(max = 12, message = "用户昵称不能超过12位")
    private String nickname;

    @NotBlank(message = "用户头像不能为空")
    private String face;

    @NotBlank(message = "真实姓名不能为空")
    private String realname;

    @Email
    @NotBlank(message = "邮件不能为空")
    private String email;

    @NotNull(message = "请选择一个性别")
    @Min(value = 0, message = "性别选择不正确")
    @Max(value = 1, message = "性别选择不正确")
    private Integer sex;

    @NotNull(message = "请选择生日日期")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") // 解决前端日期字符串传到后端后,转换为Date类型
    private Date birthday;

    @NotBlank(message = "请选择所在城市")
    private String province;

    @NotBlank(message = "请选择所在城市")
    private String city;

    @NotBlank(message = "请选择所在城市")
    private String district;
}

激活用户信息入库

service-user  com/imooc/user/controller/UserController.java
@Override
    public GraceJSONResult updateUserInfo(UpdateUserInfoBO updateUserInfoBO, BindingResult result) {
        // 0.校验BO
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        // 1.执行更新操作
        userService.updateUserInfo(updateUserInfoBO);
        return GraceJSONResult.ok();
        //调用UserService把独有信息传入
    }
service-user  com/imooc/user/service/impl/UserService.java
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);

    /**
     * 根据用户主键id查询用户信息
     * @param userId
     * @return
     */
    public AppUser getUser(String userId);

    /**
     * 用户修改信息,完善资料,并且激活
     * @param updateUserInfoBO
     */
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO);
}
service-user  com/imooc/user/service/UserServiceimpl.java
@Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);
        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
    }

查询并展示用户基本信息

service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,BindingResult result);
}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;
}Getter+Setter
service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getUserInfo(String userId) {
        //重写接口进行解耦!!
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        return GraceJSONResult.ok(userVO);
    }
}

浏览器存储介质

  • SessionStorage
  • localStorage

缓存用户信息 [用Redis减轻数据库压力]

service-api  com/imooc/api/BaseController.java
//REDIS_USER_INFO添加进来
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
    @Value("${website.domain-name}")
    public String DOMAIN_NAME;

    /**
     * 可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result) {
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList) {
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }

    public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }
service-user  com/imooc/user/controller/UserController.java
 private AppUser getUser(String userId){
        //查询判断redis中是否包含用户信息 若有则直接返回就不去查询数据库了
        String userJson = redis.get(REDIS_USER_INFO + ":" + userId);
        AppUser user = null;
        if (StringUtils.isNotBlank(userJson)){
            //字符串转换成json对象  要提取user 所以要一开始赋值null
            user = JsonUtils.jsonToPojo(userJson, AppUser.class);
        } else {
            // TODO 本方法后续公用,并且扩展
            user = userService.getUser(userId);
            // 由于用户信息不怎么会变动,对于一些千万级别网站来说,这类信息不会直接去查询数据库
            // 可以完全依靠Redis,直接把查询后的数据存入到Redis中
            // set里面设置一个key去BaseController里设置  ↓user变成jason转换类
            redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));
        }

        return user;
    }
service-user  com/imooc/user/service/UserServiceimpl.java
package com.imooc.user.service;

import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.DesensitizationUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import com.imooc.utils.DateUtil;

import java.util.Date;

@Service
public class UserServiceimpl implements UserService {
    @Autowired
    public AppUserMapper appUserMapper; //基本的CRUD都可以

    @Autowired
    public Sid sid;
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写


    @Autowired
    public RedisOperator redis;

    private static final String USER_FACE0 = "https://raw.githubusercontent.com/P-luminary/images/10d94134b65e13cc8ec9b8a9aeae4f958921cab7/data/Imooc_Cat.jpg";
    private static final String USER_FACE1 = "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg";
    @Override
    public AppUser queryMobileIsExist(String mobile) {
        Example userExample = new Example(AppUser.class);
        Example.Criteria userCriteria = userExample.createCriteria();
        userCriteria.andEqualTo("mobile", mobile);
        AppUser user = appUserMapper.selectOneByExample(userExample);
        return user;
    }

    @Transactional //对整个类的方法,事务起作用。无异常时正常提交,有异常时数据回滚
    @Override
    public AppUser createUser(String mobile) {
        /**
         * 互联网项目都要考虑可扩展性
         * 如果未来的业务激增,那么就需要分表分库
         * 那么数据库表主键id必须保证全局(全库)唯一,不得重复
         */
        String userId = sid.nextShort();
        AppUser user = new AppUser();
        user.setId(userId);
        user.setMobile(mobile);
        user.setNickname("用户:" + DesensitizationUtil.commonDisplay(mobile)); //給手机号加** 是脱敏操作
        user.setFace(USER_FACE1);
        user.setBirthday(DateUtil.stringToDate("2024-06-29")); //字符串转换Date类型
        user.setSex(Sex.secret.type);
        user.setActiveStatus(UserStatus.INACTIVE.type);//是否激活
        user.setTotalIncome(0);//收入
        user.setCreatedTime(new Date());
        user.setUpdatedTime(new Date());
        appUserMapper.insert(user);
        return user;
    }

    @Override
    public AppUser getUser(String userId) {
        return appUserMapper.selectByPrimaryKey(userId);
    }

    @Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);

        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
        // 再次查询用户的最新信息,放入redis中
        AppUser user = getUser(userId);
        redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));

    }
}
Redis里面 redis_user_info
{"id":"240629F21AK1BHX4","mobile":"15027597319","nickname":"15027597319","face":"https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg","realname":"小宝宝的小潘潘2","email":"390415030@qq.com","sex":1,"birthday":1720195200000,"province":"河北","city":"唐山市","district":"丰润区","activeStatus":1,"totalIncome":0,"createdTime":1719661387000,"updatedTime":1720281759000}

双写数据不一致的情况 [redis故障没有写入新数据]

如何双写一致 缓存双删

用户先把老Redis中的数据删除 然后再把修改值放入数据库 然后数据库再导入redis 就可以保证双写一致
但是要保证数据库放入Redis之前 后期用户请求要再其之后 [进行休眠] =>缓存双删

service-user  com/imooc/user/service/UserServiceimpl.java  @Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        // 保证双写一致,先删除redis中的数据,后更新数据库
//        redis.del(REDIS_USER_INFO + ":" + userId);

        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);

        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
        // 再次查询用户的最新信息,放入redis中
        AppUser user = getUser(userId);
        redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));

        // 缓存双删策略 [不处理可能会缓存击穿]
        try {
            Thread.sleep(100);
            redis.del(REDIS_USER_INFO + ":" + userId);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

CAP理论 [只能满足其中一、二]

分布式系统都存在CAP情况

定理

CAP的重要性

分布式不可能同时满足三个条件 【先满足P再去考虑A或C】

CAP理论是什么?
  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果 [某节点获得的数据都是一样的] 在本项目中一致性位于Session Storage
  • A(Availability, 可用性):非故障节点应该在合理的时间内作出合理的响应(不是错误或超时的响应),但是可能不是最新的数据。 [某个挂掉了 其他还可以用]
  • P(Partition tolerance, 分区容错):当出现网络分区现象后,系统能够继续运行。分区容错性

CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的
  • CP:Redis【保证数据一致性 一定要满足C】
  • AP:会采用弱一致性 淘宝下单只需要知道下单就好 数量一致性商家可以慢慢调整
  • CA:单体存在架构、关系型架构

在本项目中如果采用弱一致性:可以不把用户存到session Storage 直接显示

集群、分布式、微服务的区别

集群和分布式的区别
  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上 [服务器之间要通信]
  • 集群:同一个业务,部署在多个服务器上 [五台机器可以不通信]
集群和微服务的区别
  • 集群:分散压力
  • 微服务:分散压力
微服务和分布式的区别
  • 微服务是架构设计方式 [逻辑架构]

  • 分布式是系统部署方式 [物理架构]

  • 微服务:是一种架构方式 [大的服务拆成小的服务 每个服务独立开发测试]

  • 分布式:主要强调部署的方式

用户会话拦截器 [必须用户登陆后才可以用其他界面]

service-api  com/imooc/api/interceptors/UserTokenInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserTokenInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,访问controller之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 有些接口同时会給安卓 H5等 所以不去cookie拿
        String userId = request.getHeader("headerUserId");
        String userToken = request.getHeader("headerUserToken");

        // 判断是否放行
        boolean run = verifyUserIdToken(userId, userToken, REDIS_USER_TOKEN);

        /**
         * false:请求被拦截
         * true:请求通过验证,放行
         */
        return true;
    }


    /**
     * 请求访问到controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求访问到controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

public class BaseInterceptor {
    @Autowired
    private RedisOperator redis;
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写

    public boolean verifyUserIdToken(String id,
                                     String token,
                                     String redisKeyPrefix){ //redis..前缀
        if (StringUtils.isNotBlank(id) && StringUtils.isNotBlank(token)){
            String redisToken = redis.get(redisKeyPrefix + ":" + id);
            if (StringUtils.isBlank(id)){
                GraceException.display(ResponseStatusEnum.UN_LOGIN);
                return false;
            } else {
                if (!redisToken.equalsIgnoreCase(token)){//是否和传入token一致
                    GraceException.display(ResponseStatusEnum.TICKET_INVALID);
                    return false;
                }
            }
        }else {
            GraceException.display(ResponseStatusEnum.UN_LOGIN);
            return false;
        }
        return true;
    }
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId");
    }
}
/*
在你的 Spring 应用程序中,InterceptorConfig 类中的 @Bean 注解用于定义 PassportInterceptor 和 UserTokenInterceptor 的 bean。这使这些拦截器对象在 Spring 上下文中可用,以便进行依赖注入。

删除 @Bean 注解后会出现错误 Autowired members must be defined in valid Spring bean (@Component|@Service|...),这是因为 BaseInterceptor 类中有一个需要由 Spring 注入的依赖 (RedisOperator redis)。要让 Spring 执行依赖注入,包含 @Autowired 注解的类必须是一个由 Spring 管理的 bean,可以通过 @Component、@Service、@Controller 等注解或在配置类中通过 @Bean 来定义。

这里是对 @Bean 的作用以及为什么删除它会导致错误的详细解释:

使用 @Bean 定义 Bean:
在 InterceptorConfig 类中,@Bean 注解定义了 PassportInterceptor 和 UserTokenInterceptor 作为 Spring 的 bean。这使得它们在整个应用程序中可用于依赖注入。

依赖注入的要求:
BaseInterceptor 类中使用了 @Autowired 注解来注入 RedisOperator。要使这个注入有效,BaseInterceptor 必须是一个 Spring 管理的 bean。而 @Bean 注解在配置类中定义了这些拦截器,使得 Spring 可以管理它们,并在需要时进行依赖注入。

如果删除了 @Bean 注解,PassportInterceptor 和 UserTokenInterceptor 将不再是 Spring 管理的 bean,从而导致在它们内部或相关联的类(如 BaseInterceptor)中的依赖无法被注入。这就是为什么删除 @Bean 注解后会出现 Autowired members must be defined in valid Spring bean (@Component|@Service|...) 错误的原因。
*/

用户状态激活拦截器

service-api  com/imooc/api/interceptors/UseActiveInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户激活状态检测拦截器
 * 发文章,修改文章,删除文章,发表评论,查看评论等
 * 这些接口都是要在用户激活后才能进行操作
 * 否则需要提示用户前往[账号设置]去修改信息
 */
    public class UseActiveInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,访问controller之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 有些接口同时会給安卓 H5等 所以不去cookie拿
        String userId = request.getHeader("headerUserId");
        String userJson = redis.get(REDIS_USER_INFO + ":" + userId);
        AppUser user = null;
        if (StringUtils.isNotBlank(userJson)){
            user = JsonUtils.jsonToPojo(userJson, AppUser.class);
        } else {
            GraceException.display(ResponseStatusEnum.UN_LOGIN);
        }
        if (user.getActiveStatus() == null || user.getActiveStatus() != UserStatus.ACTIVE.type){
            GraceException.display(ResponseStatusEnum.USER_INACTIVE_ERROR);
            return false;
            //随后去拦截器里进行@Bean注册 [下下个代码就是]
        }

        /**
         * false:请求被拦截
         * true:请求通过验证,放行
         */
        return true;
    }

    /**
     * 请求访问到controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    /**
     * 请求访问到controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
service-user  com/imooc/user/controller/PassportController.java
//[加一行redis.set(REDIS_USER_INFO+":"+user.getId(), JsonUtils.objectToJson(user));]
@Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result, HttpServletRequest request, HttpServletResponse response) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }

        // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis
            redis.set(REDIS_USER_INFO+":"+user.getId(), JsonUtils.objectToJson(user));

            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi
            setCookie(request, response,"utoken",uToken,COOKIE_MONTH);
            setCookie(request, response,"uid",user.getId(),COOKIE_MONTH);
        }
        // 4.用户登录或注册成功以后,需要删除redis中的短信验证码,验证码只能使用一次,用过则作废
//        redis.del(MOBILE_SMSCODE + ":" + mobile);
        // 5.返回用户状态 返回前端看
        return GraceJSONResult.ok(userActiveStatus);
    }
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId");
//        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

AOP警告日志监控与sql打印 [切面AOP通知编程]

dev-common 引入aop依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
service-api  com/imooc/api/aspect/ServiceLogAspect.java
package com.imooc.api.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceLogAspect {
    final static Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
    /**
     * AOP通知:
     * 1.前置通知
     * 2.后置通知
     * 3.环绕通知 ★★
     * 4.异常通知
     * 5.最终通知
     */
    //*是返回所有类型 匹配包的位置 *.* = 任意文件.任意后缀  (..)是任意类和任意方法
    @Around("execution(* com.imooc.*.service.impl..*.*(..))")
    public Object recordTimeOfService(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("==== 开始执行 {}.{} ====",
                joinPoint.getTarget().getClass(),
                joinPoint.getSignature().getName());
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        long takeTime = end - start;
        if (takeTime > 3000){
            logger.error("当前执行耗时:{}",takeTime);
        }else if (takeTime > 2000){
            logger.warn("当前执行耗时:{}",takeTime);
        }else {
            logger.info("当前执行耗时:{}",takeTime);
        }
        return result;
    }
}
====================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
提交信息 看后台Terminal
service-user  application-dev.yml #增加一个open mybatis log in dev
  server:
  port: 8003

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

===================================================================
如果在未来发生sql错误可以通过sql输出来找到sql语句从而放入运行检查错误 (21 28行)
JDBC Connection [HikariProxyConnection@2054571226 wrapping org.mariadb.jdbc.MariaDbConnection@4b4b68f8] will not be managed by Spring
==>  Preparing: UPDATE app_user SET nickname = ?,face = ?,realname = ?,email = ?,sex = ?,birthday = ?,province = ?,city = ?,district = ?,active_status = ?,updated_time = ? WHERE id = ? 
==> Parameters: 15027597319(String), https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg(String), 小宝宝的小潘潘(String), 390415030@qq.com(String), 1(Integer), 2024-07-06 00:00:00.0(Timestamp), 河北(String), 唐山市(String), 丰润区(String), 1(Integer), 2024-07-07 22:41:09.862(Timestamp), 240629F21AK1BHX4(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9176eb0]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bd9477] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1798162927 wrapping org.mariadb.jdbc.MariaDbConnection@4b4b68f8] will not be managed by Spring
==>  Preparing: SELECT id,mobile,nickname,face,realname,email,sex,birthday,province,city,district,active_status,total_income,created_time,updated_time FROM app_user WHERE id = ? 
==> Parameters: 240629F21AK1BHX4(String)
<==    Columns: id, mobile, nickname, face, realname, email, sex, birthday, province, city, district, active_status, total_income, created_time, updated_time
<==        Row: 240629F21AK1BHX4, 15027597319, 15027597319, https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg, 小宝宝的小潘潘, 390415030@qq.com, 1, 2024-07-06, 河北, 唐山市, 丰润区, 1, 0, 2024-06-29 19:43:07.0, 2024-07-07 22:41:09.0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bd9477]
41:10.009 [http-nio-8003-exec-3] INFO  io.lettuce.core.EpollProvider - Starting without optional epoll library
41:10.010 [http-nio-8003-exec-3] INFO  io.lettuce.core.KqueueProvider - Starting without optional kqueue library
41:10.460 [http-nio-8003-exec-3] INFO  c.imooc.api.aspect.ServiceLogAspect - 当前执行耗时:601

退出登录、注销会话

service-api  com/imooc/api/controller/user/PassportControllerApi.java
//用户登录信息的redis和cookies清除
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO
            , BindingResult result, HttpServletRequest request, HttpServletResponse response);
    //完成之后 去BaseController里面写一个setCookie()方便都可以用

    @ApiOperation(value = "用户退出登录",notes = "用户退出登录",httpMethod = "POST")
    @PostMapping("/logout")
    public GraceJSONResult logout(@RequestParam String userId,
                                  HttpServletRequest request,
                                  HttpServletResponse response);
}
service-user  com/imooc/user/controller/PassportController.java
@Override
    public GraceJSONResult logout(String userId,
                                  HttpServletRequest request,
                                  HttpServletResponse response){
        redis.del(REDIS_USER_TOKEN + ":" + userId);
        //USER_INFO可以不用删 可能后面会查询 没有清除cookie只有重新设置时间为0
        setCookie(request, response, "utoken","",COOKIE_DELETE);
        setCookie(request, response, "uid","",COOKIE_DELETE);
        return GraceJSONResult.ok();
    }

FastDFS架构原理与上传下载流程解析

文件服务器
  • 传统服务器存储

  • FastDFS
  • OSS

  • GridFS

配置FastDFS环境准备工作

环境准备
名称 说明
Centos 7.x
libfastcommon-1.0.42.tar.gz FastDFS分离出的一些公用函数包
FastDFS FastDFS本体
fastdfs-nginx-module-1.22.tar.gz FastDFS和nginx的关联模块
nginx nginx1.15.4
它跟我说要准备两个虚拟机[tracker 和 storage 版本都是CentOS 7.x]
//创建一个FastDFS文件夹
[imooc@imooc FastDFS]$ ll
总用量 1980
-rw-rw-r--. 1 imooc imooc  800157 7月   9 15:53 fastdfs-6.04.tar.gz
-rw-rw-r--. 1 imooc imooc   19952 7月   9 15:53 fastdfs-nginx-module-1.22.tar.gz
-rw-rw-r--. 1 imooc imooc  164704 7月   9 15:53 libfastcommon-1.0.42.tar.gz
-rw-rw-r--. 1 imooc imooc 1032630 7月   9 15:53 nginx-1.16.1.tar.gz
[imooc@imooc FastDFS]$ tar -zxvf libfastcommon-1.0.42.tar.gz 
[imooc@imooc FastDFS]$ cd libfastcommon-1.0.42/
[imooc@imooc libfastcommon-1.0.42]$ ll
总用量 32
drwxrwxr-x. 2 imooc imooc   114 12月  5 2019 doc
-rw-rw-r--. 1 imooc imooc 10054 12月  5 2019 HISTORY
-rw-rw-r--. 1 imooc imooc   674 12月  5 2019 INSTALL
-rw-rw-r--. 1 imooc imooc  1607 12月  5 2019 libfastcommon.spec
-rwxrwxr-x. 1 imooc imooc  3253 12月  5 2019 make.sh
drwxrwxr-x. 2 imooc imooc   191 12月  5 2019 php-fastcommon
-rw-rw-r--. 1 imooc imooc  2776 12月  5 2019 README
drwxrwxr-x. 3 imooc imooc  4096 12月  5 2019 src
[imooc@imooc libfastcommon-1.0.42]$ ./make.sh
[imooc@imooc libfastcommon-1.0.42]$ sudo ./make.sh install
//安装解压包的本体
[imooc@imooc FastDFS]$ tar -zxvf fastdfs-6.04.tar.gz 
[imooc@imooc FastDFS]$ cd fastdfs-6.04/
[imooc@imooc fastdfs-6.04]$ ./make.sh
[imooc@imooc fastdfs-6.04]$ sudo ./make.sh install
[imooc@imooc fastdfs-6.04]$ cd /usr/bin
[imooc@imooc bin]$ ls fdfs_*
fdfs_appender_test   fdfs_download_file        fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
[imooc@imooc bin]$ cd /etc/fdfs/
[imooc@imooc fdfs]$ ll
总用量 28 //这些都是配置文件 如果要修改则需要拷贝一份新鲜的
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample

[imooc@imooc FastDFS]$ cd fastdfs-6.04/
[imooc@imooc fastdfs-6.04]$ cd conf/
[imooc@imooc conf]$ ll
总用量 88
-rw-rw-r--. 1 imooc imooc 23981 12月  5 2019 anti-steal.jpg
-rw-rw-r--. 1 imooc imooc  1834 12月  5 2019 client.conf
-rw-rw-r--. 1 imooc imooc   955 12月  5 2019 http.conf
-rw-rw-r--. 1 imooc imooc 31172 12月  5 2019 mime.types
-rw-rw-r--. 1 imooc imooc 10085 12月  5 2019 storage.conf
-rw-rw-r--. 1 imooc imooc   527 12月  5 2019 storage_ids.conf
-rw-rw-r--. 1 imooc imooc  8038 12月  5 2019 tracker.conf
//拷贝到etc下  安装前的准备工作
[imooc@imooc conf]$ sudo cp * /etc/fdfs/
[imooc@imooc conf]$ cd /etc/fdfs
[imooc@imooc fdfs]$ ll
总用量 116
-rw-r--r--. 1 root root 23981 7月   9 16:06 anti-steal.jpg
-rw-r--r--. 1 root root  1834 7月   9 16:06 client.conf
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root   955 7月   9 16:06 http.conf
-rw-r--r--. 1 root root 31172 7月   9 16:06 mime.types
-rw-r--r--. 1 root root 10085 7月   9 16:06 storage.conf
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:06 storage_ids.conf
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:06 tracker.conf
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample

配置tracker服务 [一个虚拟机]

//根据配置文件去区分是哪个服务
[imooc@imooc fdfs]$ cd /etc/fdfs
[imooc@imooc fdfs]$ ll
总用量 116
-rw-r--r--. 1 root root 23981 7月   9 16:06 anti-steal.jpg
-rw-r--r--. 1 root root  1834 7月   9 16:06 client.conf
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root   955 7月   9 16:06 http.conf
-rw-r--r--. 1 root root 31172 7月   9 16:06 mime.types
-rw-r--r--. 1 root root 10085 7月   9 16:06 storage.conf
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:06 storage_ids.conf
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:06 tracker.conf
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample
[imooc@imooc fdfs]$ sudo vim tracker.conf  
//里面的port=22122 bind_addr= 计算机节点 这些不动
//修改里面的base_path=/home/yuqing/fastdfs 
    //修改为→ /usr/local/fastdfs/tracker
[imooc@imooc fdfs]$ mkdir /usr/local/fastdfs/tracker -p //-p后面文件夹做递归创建
[imooc@imooc fdfs]$ sudo /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf //当成配置文件加进去 ★★★★★★★★★★★★★★★★★★
[imooc@imooc fdfs]$ ps -ef|grep tracker
root       6254      1  0 18:31 ?        00:00:00 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf
imooc      6268   3011  0 18:31 pts/0    00:00:00 grep --color=auto tracker

配置storage服务 [另一个虚拟机]

[storage@imooc fdfs]$ cd /etc/fdfs/
[storage@imooc fdfs]$ sudo vim storage.conf
//[修改后] group_name=imooc 
//[修改后] bath_path=/usr/local/fastdfs/storage
[storage@imooc fdfs]$ sudo mkdir /usr/local/fastdfs/storage -p
[storage@imooc fdfs]$ cd /usr/local/
[storage@imooc local]$ ll
[storage@imooc local]$ cd fastdfs/
[storage@imooc fastdfs]$ ll
[storage@localhost fastdfs]$ ll
总用量 0
drwxr-xr-x. 2 root root 6 7月   9 18:38 storage
[storage@localhost fastdfs]$ cd /etc/fdfs/  //接着修改storage
[storage@imooc fdfs]$ sudo vim storage.conf
//[修改后] store_path0=/usr/local/fastdfs/storage
//配置到tracker的ip地址[修改后] tracker_server=192.168.170.135:22122
/ ‘/8888’  http.server_port=8888 是web的相关端口号
[storage@localhost fdfs]$ sudo /usr/bin/fdfs_storaged /etc/fdfs/storage.conf //★★★★
★一定要先启动tracker 再去启动storage 不然service发不过去★
//配置客户端做上传动作
[imooc@imooc ~]$ cd /etc/fdfs
[storage@localhost fdfs]$ pwd
/etc/fdfs
[storage@localhost fdfs]$ sudo vim client.conf
//[修改后]base_path=/usr/local/fastdfs/client
[storage@localhost fdfs]$ sudo mkdir /usr/local/fastdfs/client
[storage@localhost fdfs]$ cd /usr/local/fastdfs
[storage@localhost fastdfs]$ ll
总用量 0
drwxr-xr-x. 2 root root  6 7月   9 19:26 client
drwxr-xr-x. 4 root root 30 7月   9 19:04 storage
[storage@localhost fastdfs]$ cd /etc/fdfs/
[storage@localhost fdfs]$ sudo vim client.conf
//[修改后]tracker_server=192.168.170.135:22122
[storage@localhost fdfs]$ cd /usr/bin
[storage@localhost bin]$ ls fdfs*
fdfs_appender_test   fdfs_download_file        fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
//fdfs_test在命令行去测试
[storage@localhost bin]$ cd /home/
[storage@localhost home]$ cd /usr/local/fastdfs/storage/
[storage@localhost storage]$ cd data
[storage@localhost data]$ cd 00
[storage@localhost data]$ ll   【里面很多十六进制数据】
[storage@localhost data]$ cd 00 
[storage@localhost data]$ ll   //【里面没有数据 上传图片到这里查看是否成功】
///home/storage 这里有一张测试图片log.png [自行添加]
[storage@localhost 00]$ pwd
/usr/local/fastdfs/storage/data/00/00
[storage@localhost 00]$ cd /etc/fdfs
[storage@localhost ~]$ cd /etc/fdfs/
[storage@localhost fdfs]$ cd /usr/bin/
[storage@localhost bin]$ ls fdfs*
fdfs_appender_test   fdfs_download_file      //fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
[storage@localhost bin]$ ./fdfs_test /etc/fdfs/client.conf upload /home/storage/log.png 
/*
This is FastDFS client test program v6.04

Copyright (C) 2008, Happy Fish / YuQing

FastDFS may be copied only under the terms of the GNU General
Public License V3, which may be found in the FastDFS source kit.
Please visit the FastDFS Home Page http://www.fastken.com/ 
for more detail.

[2024-07-09 19:39:25] DEBUG - base_path=/usr/local/fastdfs/client, connect_timeout=10, network_timeout=60, tracker_server_count=1, anti_steal_token=0, anti_steal_secret_key length=0, use_connection_pool=0, g_connection_pool_max_idle_time=3600s, use_storage_id=0, storage server id count: 0

tracker_query_storage_store_list_without_group: 
    server 1. group_name=, ip_addr=192.168.170.136, port=23000

group_name=imooc【企业简写】, ip_addr=192.168.170.136, port=23000
storage_upload_by_filename
group_name=imooc, remote_filename=M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png 
【remote_filename:重组路径】【因为还没有发布文件服务 所以无法直接查看文件】
source ip address: 192.168.170.136
file timestamp=2024-07-09 19:39:25
file size=12618
file crc32=630904148
example file url: http://192.168.170.136/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
storage_upload_slave_by_filename
group_name=imooc, remote_filename=M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
source ip address: 192.168.170.136
file timestamp=2024-07-09 19:39:25
file size=12618
file crc32=630904148
example file url: http://192.168.170.136/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
 */
[storage@localhost bin]$ cd /usr/local/fastdfs/storage/data/
[storage@localhost data]$ cd 00
[storage@localhost 00]$ cd 00
[storage@localhost 00]$ ll
总用量 40
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png-m
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png-m

安装Nginx提供Web服务 [通过浏览器访问到文件]

Nginx是反向代理服务器可以做集群 也可以控制多个虚拟主机
-rw-rw-r--. 1 storage storage 142245547 7月  10 15:32 jdk-7u75-linux-x64.tar.gz
-rw-rw-r--. 1 storage storage   1032630 7月  10 15:33 nginx-1.16.1.tar.gz
//[storage@localhost ~]$ sudo yum install gcc-c++
已加载插件:fastestmirror, langpacks
Determining fastest mirrors
//[storage@localhost ~]$ sudo yum install -y pcre pcre-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ sudo yum install -y zlib zlib-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ sudo yum install -y openssl openssl-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ tar -zxvf nginx-1.16.1.tar.gz
nginx-1.16.1.tar.gz
//[storage@localhost ~]$ cd nginx-1.16.1.tar.gz
[storage@localhost nginx-1.16.1]$ sudo mkdir /var/temp/nginx -p
//创建所需的临时目录:
sudo mkdir -p /var/temp/nginx/client
sudo mkdir -p /var/temp/nginx/proxy
sudo mkdir -p /var/temp/nginx/fastcgi
sudo mkdir -p /var/temp/nginx/uwsgi
sudo mkdir -p /var/temp/nginx/scgi
[storage@localhost nginx-1.16.1]$ ./configure \  //【预配置】
> --prefix=/usr/local/nginx \
> --pid-path=/var/run/nginx/nginx.pid \
> --lock-path=/var/lock/nginx.lock \
> --error-log-path=/var/log/nginx/error.log \
> --http-log-path=/var/log/nginx/access.log \
> --with-http_gzip_static_module \
> --http-client-body-temp-path=/var/temp/nginx/client \
> --http-proxy-temp-path=/var/temp/nginx/proxy \
> --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
> --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
> --http-scgi-temp-path=/var/temp/nginx/scgi
[storage@localhost nginx-1.16.1]$ make  //【编译】
/* linux中的网络不可达
如果镜像出了问题 一定要换一下镜像配置
1. 编辑 CentOS 的 YUM 配置文件:
编辑 /etc/yum.repos.d/CentOS-Base.repo 文件:

复制代码
sudo vi /etc/yum.repos.d/CentOS-Base.repo

2. 使用以下内容更新 CentOS-Base.repo 文件:
复制代码
[base]
name=CentOS-$releasever - Base
baseurl=http://vault.centos.org/7.9.2009/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[updates]
name=CentOS-$releasever - Updates
baseurl=http://vault.centos.org/7.9.2009/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[extras]
name=CentOS-$releasever - Extras
baseurl=http://vault.centos.org/7.9.2009/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
*/
[storage@localhost nginx-1.16.1]$ sudo make install
[storage@localhost nginx-1.16.1]$ cd /usr/local
[storage@localhost local]$ ll
总用量 0
drwxr-xr-x. 2 root root  6 4月  11 2018 bin
drwxr-xr-x. 2 root root  6 4月  11 2018 etc
drwxr-xr-x. 4 root root 35 7月   9 19:26 fastdfs
drwxr-xr-x. 2 root root  6 4月  11 2018 games
drwxr-xr-x. 2 root root  6 4月  11 2018 include
drwxr-xr-x. 2 root root  6 4月  11 2018 lib
drwxr-xr-x. 2 root root  6 4月  11 2018 lib64
drwxr-xr-x. 2 root root  6 4月  11 2018 libexec
drwxr-xr-x. 5 root root 42 7月  10 16:55 nginx
drwxr-xr-x. 2 root root  6 4月  11 2018 sbin
drwxr-xr-x. 5 root root 49 7月   9 17:16 share
drwxr-xr-x. 2 root root  6 4月  11 2018 src
[storage@localhost local]$ cd nginx/
[storage@localhost nginx]$ ll
总用量 4
drwxr-xr-x. 2 root root 4096 7月  10 16:55 conf
drwxr-xr-x. 2 root root   40 7月  10 16:55 html
drwxr-xr-x. 2 root root   19 7月  10 16:55 sbin
[storage@localhost nginx]$ cd sbin/
[storage@localhost sbin]$ ll
总用量 3768
-rwxr-xr-x. 1 root root 3857144 7月  10 16:55 nginx
[storage@localhost sbin]$ sudo ./nginx
[storage@localhost sbin]$ ps -ef|grep nginx
root       6642      1  0 16:58 ?        00:00:00 nginx: master process ./nginx
nobody     6643   6642  0 16:58 ?        00:00:00 nginx: worker process
storage    6651   2975  0 16:58 pts/0    00:00:00 grep --color=auto nginx

// 在浏览器输入:http://192.168.170.136/ 【如果没显示应该是虚拟机的防火墙拦截 可以禁止防火墙】
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

[storage@localhost nginx]$ cd html
[storage@localhost html]$ ll
总用量 8
-rw-r--r--. 1 root root 494 7月  10 16:55 50x.html
-rw-r--r--. 1 root root 612 7月  10 16:55 index.html

[storage@localhost html]$ sudo ../sbin/nginx -t //【测试刚刚的步骤是否正确】
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

整合Nginx实现文件服务器

[storage@localhost FastDFS]$ tar -zxvf fastdfs-nginx-module-1.22.tar.gz 
fastdfs-nginx-module-1.22/
fastdfs-nginx-module-1.22/HISTORY
fastdfs-nginx-module-1.22/INSTALL
fastdfs-nginx-module-1.22/src/
fastdfs-nginx-module-1.22/src/common.c
fastdfs-nginx-module-1.22/src/common.h
fastdfs-nginx-module-1.22/src/config
fastdfs-nginx-module-1.22/src/mod_fastdfs.conf
fastdfs-nginx-module-1.22/src/ngx_http_fastdfs_module.c
[storage@localhost FastDFS]$ cd fastdfs-nginx-module-1.22/
[storage@localhost fastdfs-nginx-module-1.22]$ ll
总用量 8
-rw-rw-r--. 1 storage storage 3036 11月 19 2019 HISTORY
-rw-rw-r--. 1 storage storage 2001 11月 19 2019 INSTALL
drwxrwxr-x. 2 storage storage  109 11月 19 2019 src
[storage@localhost fastdfs-nginx-module-1.22]$ cd src/
[storage@localhost src]$ sudo cp mod_fastdfs.conf /etc/fdfs/
[storage@localhost ~]$ cd /etc/fdfs/
[storage@localhost fdfs]$ sudo vim mod_fastdfs.conf 
//【布置存储路径】
/*
store_path0=/usr/local/fastdfs/storage
tracker_server=192.168.170.135:22122
group_name=imooc
url_have_group_name = true
base_path=/usr/local/fastdfs/tmp
*/
[storage@localhost FastDFS]$ cd fastdfs-nginx-module-1.22/
[storage@localhost fastdfs-nginx-module-1.22]$ cd src/
[storage@localhost src]$ vim config
/local 把带有local的都删掉
[storage@localhost ~]$ cd nginx-1.16.1/
 [storage@localhost nginx-1.16.1]$ 
./configure \
> --prefix=/usr/local/nginx \
> --pid-path=/var/run/nginx/nginx.pid \
> --lock-path=/var/lock/nginx.lock \
> --error-log-path=/var/log/nginx/error.log \
> --http-log-path=/var/log/nginx/access.log \
> --with-http_gzip_static_module \
> --http-client-body-temp-path=/var/temp/nginx/client \
> --http-proxy-temp-path=/var/temp/nginx/proxy \
> --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
> --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
> --http-scgi-temp-path=/var/temp/nginx/scgi \
> --add-module=/home/storage/FastDFS/fastdfs-nginx-module-1.22/src
[storage@localhost nginx-1.16.1]$ sudo make && sudo make install
[storage@localhost nginx-1.16.1]$ cd /usr/local/nginx/
[storage@localhost nginx]$ cd conf/
[storage@localhost conf]$ sudo vim nginx.conf
/*
server {
        listen       8888;
        server_name  localhost;
        location ~/group[0-9]/ {
        ngx_fastdfs_module;
        }
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

*/
[storage@localhost conf]$ vim /etc/fdfs/tracker.conf
/http.service_port:8080
[storage@localhost conf]$ sudo vim nginx.conf //★★★★★★
/*
server {
        listen       8888;
        server_name  localhost;
        location /imooc/M00 {
            ngx_fastdfs_module;
        }
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
}
*/
[storage@localhost conf]$ sudo ../sbin/nginx -t //测试一下有无问题
ngx_http_fastdfs_set pid=6143
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
[storage@localhost conf]$ sudo ../sbin/nginx -s reload //重新加载
ngx_http_fastdfs_set pid=6436

[storage@localhost conf]$ cd /usr/local/fastdfs/storage/
[storage@localhost storage]$ cd data
[storage@localhost data]$ cd 00/00
总用量 40
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png-m
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png-m
http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png


//查看错误日志 ★★★【sudo tail -n 50 /var/log/nginx/error.log】★★★
/*
2. 重新加载 systemd 并启动 Nginx
重新加载 systemd:
复制代码
sudo systemctl daemon-reload
启动 Nginx:

sh
复制代码
sudo systemctl start nginx
设置开机自启动:

sh
复制代码
sudo systemctl enable nginx
检查 Nginx 服务状态:

sh
复制代码
sudo systemctl status nginx
*/
[storage@localhost conf]$ sudo vim /etc/fdfs/tracker.conf
[storage@localhost conf]$ sudo vim /etc/fdfs/storage.conf

[storage@localhost conf]$ cd /usr/local/nginx/conf/
[storage@localhost conf]$ sudo ../sbin/nginx -s stop
ngx_http_fastdfs_set pid=12586
[storage@localhost conf]$ sudo ../sbin/nginx
ngx_http_fastdfs_set pid=12605
[storage@localhost conf]$ sudo ../sbin/nginx -s reload

/*
FastDFS输出报告位置:
sudo tail -n 50 /usr/local/fastdfs/storage/logs/storaged.log


启动 Tracker 服务器:
sudo systemctl start fdfs_trackerd
检查 Tracker 服务器状态:
sudo systemctl status fdfs_trackerd
确认 Tracker 服务器监听端口:
sudo netstat -tuln | grep :22122
*/
//草!好几个小时的含泪史 一定要先开tracker端!!!
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★

/*
首先,重新启动 FastDFS 的 tracker 和 storage 服务:

bash
复制代码
# 重启 tracker 服务
sudo systemctl restart fdfs_trackerd

# 重启 storage 服务
sudo systemctl restart fdfs_storaged
2. 重启 Nginx 服务
接下来,重新启动 Nginx 服务,确保它能够加载新的配置并生效:

bash
复制代码
sudo systemctl restart nginx
3. 验证服务状态
重新启动服务后,可以通过以下方式验证它们的运行状态:

检查 FastDFS 服务状态:

bash
复制代码
sudo systemctl status fdfs_trackerd
sudo systemctl status fdfs_storaged
检查 Nginx 服务状态:

bash
复制代码
sudo systemctl status nginx
*/

http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png

创建文件服务module [文件上传]

【新建一个module imooc-news-dev-service-files】
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-files</artifactId>
<!--
    imooc-news-dev-service-files
    文件服务,文件相关的操作都在此文件中进行
    文件上传   文件下载
    fastdfs  oss  gridfs
-->
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 文件上传fdfs工具包 -->
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
            <version>1.27.2</version>
        </dependency>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>



    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
application.yuml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000
application-dev.yuml
server:
  port: 8004

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
service-files  com/imooc/files/controller/HelloController.java
package com.imooc.files.controller;


import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello(){
        return GraceJSONResult.ok("Hello World!");
    }
}




service-files  com/imooc/files/Application.java
package com.imooc.files;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除数据源
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
-----------------------------------------------------------------------
http://localhost:8004/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": "Hello World!"
}

整合fdfs与service实现 [文件上传]

service-api  com/imooc/files/service/impl/UploaderServiceImpl.java
package com.imooc.files.service.impl;

import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.files.service.UploaderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
public class UploaderServiceImpl implements UploaderService {
    //注入客户端
    @Autowired
    public FastFileStorageClient fastFileStorageClient;

    @Override
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException {
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileExtName, null);
        return storePath.getFullPath();
    }
}
service-api  com/imooc/files/service/UploaderService.java
package com.imooc.files.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface UploaderService {
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException;
}
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000

############################################################
#
# fdfs配置信息
#
############################################################
fdfs:
  connect-timeout: 30
  so-timeout: 30
  tracker-list: 192.168.170.135:22122

实现fastdfs图片存储 [文件上传]

service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;
}
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // 执行上传
                path = uploaderService.uploadFdfs(file, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);
        return GraceJSONResult.ok(path);
    }
}
//此时去上传图片会报错 报跨域异常错误
//需要在用户service-user里的Application 
//@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
//贴到com/imooc/files/Application.java

此时再次 http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
提交头像
Console:
06:09.827 [http-nio-8004-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
06:09.827 [http-nio-8004-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
06:09.832 [http-nio-8004-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 5 ms
06:09.882 [http-nio-8004-exec-2] INFO  c.imooc.api.aspect.ServiceLogAspect - ==== 开始执行 class com.imooc.files.service.impl.UploaderServiceImpl.uploadFdfs ====
06:09.937 [http-nio-8004-exec-2] INFO  c.imooc.api.aspect.ServiceLogAspect - 当前执行耗时:55
06:09.937 [http-nio-8004-exec-2] INFO  c.i.f.c.FileUploadController - path = imooc/M00/00/00/wKiqiGaPrpKAEt22AAAeb3kUsrg507.png

http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaPrpKAEt22AAAeb3kUsrg507.png
此时就可以看到Cat的图片了!

完善用户头像上传

【在用户返回的时候写死路径+path】
return GraceJSONResult.ok("http://192.168.170.136:8888/"path);

给它包装一下 FileResource写一下
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.files.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // 执行上传
                path = uploaderService.uploadFdfs(file, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
            finalPath = fileResource.getHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}
service-files  com/imooc/files/FileResource.java
package com.imooc.files;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }
}
file-dev.properties
# fastdfs storage 节点地址(nginx整合的web服务)
file.host=http://192.168.170.136:8888/


application.yml
############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000
service-api  com/imooc/api/config/InterceptorConfig.java
此时拦截器也要加一层
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
//        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

图片大小控制的统一异常处理

dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice
public class GraceExceptionHandler {
    @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseBody
    public GraceJSONResult returnMaxUploadSizeExceededException(MaxUploadSizeExceededException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_MAX_SIZE_ERROR);
    }
}

第三方云存储解决方案 【阿里OSS】

FastDFS [中小型公司使用]
  • 水平扩容
  • 运维复杂
  • 开发复杂【增加图片效果 & 人脸识别等】
云存储阿里OSS
  • SDK使用简单 [Java对接]
  • 提供强大的文件处理功能
  • 零运维成本
  • 图形化管理控制台
  • CDN加速
  • 降低风险管理成本

对象存储 OSS 资源包 (aliyun.com)[购买 标准-本地冗余存储下行流量 ]

控制台的基本配置使用 【阿里OSS】

OSS管理控制台 (aliyun.com)

费用与成本 (aliyun.com)我的试用

  • 对象存储OSS → Bucket列表 → 创建Bucket → 存储冗余类型:本地冗余存储 → 读写权限:公共读

OSS管理控制台 (aliyun.com)

dev-common  pom.xml
<dependency>
  <groupId>com.aliyun.oss</groupId>
  <artifactId>aliyun-sdk-oss</artifactId>
  <version>3.10.2</version>
</dependency>
  • 对象存储OSS → SDK文档 → 上传网络流
  • 对象存储OSS → iimooc-news-dev → 概览 → 访问端口: 外网访问 oss-cn-shanghai.aliyuncs.com

SDK的使用与项目整合

service-file  com/imooc/files/service/UploaderService.java
package com.imooc.files.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface UploaderService {
    /**
     * 使用fastdfs 上传文件
     */
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException;

    /**
     * 使用OSS 上传文件
     */
    public String uploadOSS(MultipartFile file,String userId, String fileExtName) throws IOException;
}
service-files  com/imooc/files/service/impl/UploaderServiceImpl.java
package com.imooc.files.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.utils.extend.AliyunResource;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

@Service
public class UploaderServiceImpl implements UploaderService {
    //注入客户端
    @Autowired
    public FastFileStorageClient fastFileStorageClient;
    @Autowired
    public FileResource fileResource;
    @Autowired
    public AliyunResource aliyunResource;
    @Autowired
    public Sid sid;

    @Override
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException {
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileExtName, null);
        return storePath.getFullPath();
    }

    @Override
    public String uploadOSS(MultipartFile file, String userId, String fileExtName) throws IOException {
        // Endpoint以杭州为例,其它Region请按实际情况填写。
        // 外网访问:oss-cn-shanghai.aliyuncs.com
        String endpoint = fileResource.getEndpoint();
        // 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
        String accessKeyId = aliyunResource.getAccessKeyID();
        String accessKeySecret = aliyunResource.getAccessKeySecret();

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint,
                accessKeyId,
                accessKeySecret);
        //  images/abc/10010/cat.png  路径不全 所以需要myObjectName拼接
        String fileName = sid.nextShort();
        String myObjectName = fileResource.getObjectName()
                + "/" + userId + "/" + fileName + "." + fileExtName;

        // 上传网络流。
        InputStream inputStream = file.getInputStream();
        ossClient.putObject(fileResource.getBucketName(),
                myObjectName,
                inputStream);

        // 关闭OSSClient。
        ossClient.shutdown();
        return myObjectName;
    }
}
service-files  com/imooc/files/resource/FileResource.java
package com.imooc.files.resource;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;
    private String endpoint;
    private String BucketName;
    private String objectName;
}Getter + Setter
file-dev.properties
# fastdfs storage ????(nginx???web??)
file.host=http://192.168.170.136:8888/

# aliyun OSS
file.endpoint=oss-cn-shanghai.aliyuncs.com

file.BucketName=iimooc-news-dev

# url name
file.objectName=images/abc

OSS整合实现文件上传

 // OSS执行上传
//  path = uploaderService.uploadOSS(file, userId, suffix);

service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
            finalPath = fileResource.getHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}

=================================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
此时更改头像上传后 头像会在OSS服务器的文件里面显示
file-dev.properties / file-prod.properties
# fastdfs storage ????(nginx???web??)
file.host=http://192.168.170.136:8888/

# aliyun OSS
file.endpoint=oss-cn-shanghai.aliyuncs.com

file.BucketName=iimooc-news-dev

# url name
file.objectName=images/abc

file.ossHost=https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/
service-files  com/imooc/files/resource/FileResource.java
@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;
    private String endpoint;
    private String BucketName;
    private String objectName;
    private String OssHost;
}Getter + Setter
service-files  com/imooc/files/controller/FileUploadController.java
    //用OSS执行上传 而不是 fdfs执行上传
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
            finalPath = fileResource.getOssHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}

图片自动审核 【阿里内容安全】

多媒体内容风险智能识别服务,降低色情、暴力、恐怖 (由于太贵了就不买了 1000多呢)
dev-common  pom.xml
 <!-- 第三方云厂商相关的依赖 -->

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-green</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
dev-common  com/imooc/utils/extend/AliImageReviewUtils.java
package com.imooc.utils.extend;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.imooc.enums.ArticleReviewLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Date;
import java.util.UUID;

@Component
public class AliImageReviewUtils {

//    文档地址:https://help.aliyun.com/document_detail/70292.html?spm=a2c4g.11186623.2.49.6f9c75fdjaW30p#reference-fzy-ztm-v2b

    @Autowired
    private AliyunResource aliyunResource;

    public boolean reviewImage(String imgUrl) throws Exception {
        IClientProfile profile = DefaultProfile
                .getProfile("cn-shanghai", "", "");
        DefaultProfile
                .addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
        IAcsClient client = new DefaultAcsClient(profile);

        ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
        // 指定api返回格式
        imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
        // 指定请求方法
        imageSyncScanRequest.setMethod(MethodType.POST);
        imageSyncScanRequest.setEncoding("utf-8");
        //支持http和https
        imageSyncScanRequest.setProtocol(ProtocolType.HTTP);


        JSONObject httpBody = new JSONObject();
        /**
         * 设置要检测的场景, 计费是按照该处传递的场景进行
         * 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算
         * 例如:检测2张图片,场景传递porn、terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算
         * porn: porn表示色情场景检测
         * logo: 商标
         * 其他详见官方文档
         */
        httpBody.put("scenes", Arrays.asList("terrorism"));

        /**
         * 设置待检测图片, 一张图片一个task
         * 多张图片同时检测时,处理的时间由最后一个处理完的图片决定
         * 通常情况下批量检测的平均rt比单张检测的要长, 一次批量提交的图片数越多,rt被拉长的概率越高
         * 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task
         */
        JSONObject task = new JSONObject();
        task.put("dataId", UUID.randomUUID().toString());

        //设置图片链接
        task.put("url", imgUrl);
        task.put("time", new Date());
        httpBody.put("tasks", Arrays.asList(task));

        imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
                "UTF-8", FormatType.JSON);

        /**
         * 请设置超时时间, 服务端全链路处理超时时间为10秒,请做相应设置
         * 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常
         */
        imageSyncScanRequest.setConnectTimeout(3000);
        imageSyncScanRequest.setReadTimeout(10000);
        HttpResponse httpResponse = null;
        try {
            httpResponse = client.doAction(imageSyncScanRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //服务端接收到请求,并完成处理返回的结果
        if (httpResponse != null && httpResponse.isSuccess()) {
            JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
            System.out.println(JSON.toJSONString(scrResponse, true));
            int requestCode = scrResponse.getIntValue("code");
            //每一张图片的检测结果
            JSONArray taskResults = scrResponse.getJSONArray("data");
            if (200 == requestCode) {
                for (Object taskResult : taskResults) {
                    //单张图片的处理结果
                    int taskCode = ((JSONObject) taskResult).getIntValue("code");
                    //图片要检测的场景的处理结果, 如果是多个场景,则会有每个场景的结果
                    JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
                    if (200 == taskCode) {
                        Object sceneResult = sceneResults.get(0);
//                        for (Object sceneResult : sceneResults) {
                        String scene = ((JSONObject) sceneResult).getString("scene");
                        String suggestion = ((JSONObject) sceneResult).getString("suggestion");
                        //根据scene和suggetion做相关处理
                        //do something
                        System.out.println("scene = [" + scene + "]");
                        System.out.println("suggestion = [" + suggestion + "]");

                        return suggestion.equalsIgnoreCase(ArticleReviewLevel.PASS.type) ? true : false;
//                        }
                    } else {
                        //单张图片处理失败, 原因视具体的情况详细分析
                        System.out.println("task process fail. task response:" + JSON.toJSONString(taskResult));
                        return false;
                    }
                }
            } else {
                /**
                 * 表明请求整体处理失败,原因视具体的情况详细分析
                 */
                System.out.println("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
                return false;
            }
        }
        return false;
    }
}

解决github Push rejected报错 remote: error: GH013: Repository rule violations found for refs/heads/maste_github push declined due to repository rule violat-CSDN博客

dev-common  com/imooc/enums/ArticleReviewLevel.java
package com.imooc.enums;

/**
 * @Desc: 文章自动审核结果 枚举
 */
public enum ArticleReviewLevel {
    PASS("pass", "自动审核通过"),
    BLOCK("block", "自动审核不通过"),
    REVIEW("review", "建议人工复审");

    public final String type;
    public final String value;

    ArticleReviewLevel(String type, String value) {
        this.type = type;
        this.value = value;
    }
}
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;

import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.extend.AliImageReviewUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Autowired
    private AliImageReviewUtils aliImageReviewUtils;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传     要让外面得以访问 ①需要把内网的环境发布到公网 [内网穿透]  ②路由器端口映射到外网  ③fastdfs安装到公网里
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
            finalPath = fileResource.getOssHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
           return GraceJSONResult.ok(finalPath);
       // return GraceJSONResult.ok(doAliImageReview(finalPath)); //这里加了图片审核咯
    }

    /**
     * fastdfs 默认存在于内网,无法被阿里云内容管理服务检查到
     * 需要配置到公网才行:
     * 1. 内网穿透,natppp/花生壳/ngrok
     * 2. 路由配置端口映射
     * 3. fdfs 发布到云服务器
     */

   /* 功能实现不了图片识别 因为没有开通内容安全需要企业认证
     public static final String FAILED_IMAGE_URL = "https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/240629F21AK1BHX4/Review_Failed.png"; //这里保存审核失败的照片 提前上传到Oss里直接用
    private String doAliImageReview(String pendingImageUrl){
        boolean result = false;
        try {
            result = aliImageReviewUtils.reviewImage(pendingImageUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!result){
            return FAILED_IMAGE_URL;
        }
        return pendingImageUrl;
    }
}
*/

创建阿里云子账号 【阿里内容安全】

RAM访问控制 → 用户
[实在不行了 凑合着搞一下吧 功能实现不了图片识别 因为没有开通内容安全需要企业认证]

登录名称:imooc-news-dev
显示名称:用于内容审核
√ OpenAPI访问调用
AccessKey ID:
AccessKeySeret:

点击左侧列表下方 授权 → 新增授权 【授权主体:用于内容审核 权限策略:搜:green … 管理内容安全的权限】

构建admin服务

  • 构建admin管理服务
  • 文章分类管理
  • 友情连接管理
  • 用户账号管理
  • 文章内容人工审核 [放在文章上传后的自动审核]
  • admin管理人员账号分配 [用户人脸]
service-admin  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-admin</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


</project>
service-admin  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-admin"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-admin.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-admin:8005
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-admin  com/imooc/admin/controller/HelloController.java
package com.imooc.admin.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}
----------------------------------------------------------------------------
http://admin.imoocnews.com:8005/hello

application-dev.yml
server:
  port: 8005

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

表设计与账号预分配 【admin账号】

service-admin  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
service-admin  com/imooc/admin/controller/PWDTest.java
package com.imooc.admin.controller;

import org.springframework.security.crypto.bcrypt.BCrypt;

public class PWDTest {
    public static void main(String[] args) {
        String pwd = BCrypt.hashpw("admin", BCrypt.gensalt());//加盐
        System.out.println(pwd);
    }
}

持久层查询管理员 【admin账号】

http://admin.imoocnews.com:9090/imooc-news/admin/login.html
Windows下如何查看某个端口被谁占用 | 菜鸟教程 (runoob.com)

//更改一下mybatis-generator-database里面的generatorConfig-admin.xml
数据库表为:admin_user
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.admin" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.admin.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="admin_user"></table>

    </context>
</generatorConfiguration>
mybatis-generator-database的把AdminUser拷贝到dev-model的com/imooc/pojo下
mybatis-generator-database的把AdminUserMapper拷贝到service-admin的resources mapper/AdminUserMapper.xml
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);
}



service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.pojo.AdminUser;
import org.springframework.beans.factory.annotation.Autowired;
import tk.mybatis.mapper.entity.Example;

public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }
}
service-admin  com/imooc/admin/mapper/AdminUserMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.AdminUser;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminUserMapper extends MyMapper<AdminUser> {
}
service-admin  mapper/AdminUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.AdminUserMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.AdminUser" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="username" property="username" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="face_id" property="faceId" jdbcType="VARCHAR" />
    <result column="admin_name" property="adminName" jdbcType="VARCHAR" />
    <result column="created_time" property="createdTime" jdbcType="TIMESTAMP" />
    <result column="updated_time" property="updatedTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>

用户名密码登录 【admin账号】

Spring里遇到的一个问题,autowired时报找不到bean定义_autowired找不到bean-CSDN博客

其次上述问题一定要去找Controller Service ServiceImpl 和 启动类里面的有没有正确扫描包@MapperScan(basePackages = “com.imooc.admin.mapper”)
service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private RedisOperator redis;

    @Override
    public Object adminLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1.查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2.判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
        // 3.判断密码是否匹配
        boolean isPwdMath = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMath){
            doLoginSettings(admin,request,response);
            return GraceJSONResult.ok();
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }

    /**
     * 用于admin用户登录过后的基本信息设置
     */
    private void doLoginSettings(AdminUser admin, HttpServletRequest request, HttpServletResponse response){
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(),token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }
}
---------------------------------------------------------------------------------
http://admin.imoocnews.com:9090/imooc-news/admin/login.html
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.pojo.AdminUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

@Service
public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }
}
service-admin  com/imooc/admin/mapper/AdminUserMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.AdminUser;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminUserMapper extends MyMapper<AdminUser> {
}


service-admin  resources/mapper/AdminUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.AdminUserMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.AdminUser" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="username" property="username" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="face_id" property="faceId" jdbcType="VARCHAR" />
    <result column="admin_name" property="adminName" jdbcType="VARCHAR" />
    <result column="created_time" property="createdTime" jdbcType="TIMESTAMP" />
    <result column="updated_time" property="updatedTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>
service-admin  com/imooc/admin/Application.java
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.admin.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-admin  application-dev.yml
server:
  port: 8005

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
## setup CN from java, This is resource
website:
  domain-name: imoocnews.com

## open mybatis log in dev
#mybatis:
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#

校验admin账号唯一 【admin账号】

http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html

service-admin  com/imooc/admin/controller/AdminMngController.java

  @Override
    public Object adminLogin(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }
    private void checkAdminExist(String username){
        AdminUser admin = adminUserService.queryAdminByUsername(username);
        if (admin != null){
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }

    }
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.pojo.bo.AdminLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public Object adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                             HttpServletRequest request,
                             HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public Object adminLogin(@RequestParam String username); //传回来
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");

        //        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

创建admin账号 【admin账号】

用户管理 | 运营管理平台 (imoocnews.com)
http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html

service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private RedisOperator redis;

    @Override
    public GraceJSONResult adminLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1.查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2.判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
        // 3.判断密码是否匹配
        boolean isPwdMath = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMath){
            doLoginSettings(admin,request,response);
            return GraceJSONResult.ok();
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }


    /**
     * 用于admin用户登录过后的基本信息设置
     */
    private void doLoginSettings(AdminUser admin, HttpServletRequest request, HttpServletResponse response){
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(),token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }


    @Override
    public GraceJSONResult adminLogin(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }

    private void checkAdminExist(String username){
        AdminUser admin = adminUserService.queryAdminByUsername(username);
        if (admin != null){
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }

    }

    @Override
    public GraceJSONResult addNewAdmin(NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. base64不为空,则代表人脸入库,否则需要用户输入密码和确认密码
        if (StringUtils.isBlank(newAdminBO.getImg64())){
            if (StringUtils.isBlank(newAdminBO.getPassword()) || StringUtils.isBlank(newAdminBO.getConfirmPassword())){
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_NULL_ERROR);
            }
        }
        // 2. 密码不为空,则必须判断两次输入一致
        if (StringUtils.isNotBlank(newAdminBO.getPassword())) {
            if (!newAdminBO.getPassword().equalsIgnoreCase(newAdminBO.getConfirmPassword())) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_ERROR);
            }
        }
        // 3. 校验用户名唯一
        checkAdminExist(newAdminBO.getUsername());

        // 4.调用service存入admin信息
        adminUserService.createAdminUser(newAdminBO);
        return GraceJSONResult.ok();
    }

}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminLogin(@RequestParam String username); //传回来

    @ApiOperation(value = "创建admin",notes = "创建admin",httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response); //传回来
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;

@Service
public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Autowired
    public Sid sid;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }

    @Override
    public void createAdminUser(NewAdminBO newAdminBO) {
        String adminId = sid.nextShort(); //获得主键
        AdminUser adminUser = new AdminUser();
        adminUser.setId(adminId);
        adminUser.setUsername(newAdminBO.getUsername());
        adminUser.setAdminName(newAdminBO.getAdminName());
        // 如果密码不为空 则密码需要加密 存入数据库
        if (StringUtils.isNotBlank(newAdminBO.getPassword())){
            String pwd = BCrypt.hashpw(newAdminBO.getPassword(), BCrypt.gensalt());
            adminUser.setPassword(pwd);
        }

        // 如果人脸上传以后,则有faceId,需要和admin信息关联存储入库
        if (StringUtils.isNotBlank(newAdminBO.getFaceId())){
            adminUser.setFaceId(newAdminBO.getFaceId());
        }
        adminUser.setCreatedTime(new Date());
        adminUser.setUpdatedTime(new Date());

        int insert = adminUserMapper.insert(adminUser);
        if (insert != 1){
            GraceException.display(ResponseStatusEnum.ADMIN_CREATE_ERROR);
        }
    }
}
com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);
}

查看admin列表 【admin账号】(分页查询)

service-admin  com/imooc/admin/controller/AdminMngController.java
 @Override
    public GraceJSONResult getAdminList(Integer page, Integer pageSize) {
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){//由于是固定数值 可以去basecontroller加一下
            pageSize = COMMON_PAGE_SIZE;
        }
        adminUserService.queryAdminList(page, pageSize);
        return GraceJSONResult.ok();
    }
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);


    /**
     * 分页查询admin列表
     * @param page
     * @param pageSize
     */
    public void queryAdminList(Integer page, Integer pageSize);
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
@Override
    public void queryAdminList(Integer page, Integer pageSize) {
        Example adminExample = new Example(AdminUser.class);
        adminExample.orderBy("createdTime").asc();
        PageHelper.startPage(page, pageSize);
        List<AdminUser> adminUserList = adminUserMapper.selectByExample(adminExample);
        System.out.println(adminUserList);
    } //下面一节会有改动
service-api  com/imooc/api/config/Swagger2.java
package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
    //    http://localhost:8088/doc.html            新路径

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
//        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
//                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminLogin(@RequestParam String username); //传回来

    @ApiOperation(value = "创建admin",notes = "创建admin",httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response);

    @ApiOperation(value = "查询admin列表",notes = "查询admin列表",httpMethod = "POST")
    @PostMapping("/getAdminList")
    public GraceJSONResult getAdminList(@ApiParam(name = "page", value = "查询下一页的第几页",  required = false) @RequestParam Integer page,
                                        @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false) @RequestParam Integer pageSize);
}

-----------------------------------------------------------------
http://admin.imoocnews.com:8005/doc.html

封装PagedGridResult分页数据_调试分页接口 【admin账号】

service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
    @Override
    public PagedGridResult queryAdminList(Integer page, Integer pageSize) {
        Example adminExample = new Example(AdminUser.class);
        adminExample.orderBy("createdTime").asc();
        PageHelper.startPage(page, pageSize);
        List<AdminUser> adminUserList = adminUserMapper.selectByExample(adminExample);
        return setterPagedGrid(adminUserList, page);
    }

    private PagedGridResult setterPagedGrid( List<?> adminUserList, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(adminUserList);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(adminUserList);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getPages());
        gridResult.setTotal(pageList.getTotal());
        return gridResult;

    }
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.PagedGridResult;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);


    /**
     * 分页查询admin列表
     * @param page
     * @param pageSize
     */
    public PagedGridResult queryAdminList(Integer page, Integer pageSize);
}
dev-common  com/imooc/utils/PagedGridResult.java
package com.imooc.utils;

import java.util.List;

/**
 * 
 * @Title: PagedGridResult.java
 * @Package com.imooc.utils
 * @Description: 用来返回分页Grid的数据格式
 * Copyright: Copyright (c) 2019
 */
public class PagedGridResult {
    
    private int page;            // 当前页数
    private long total;            // 总页数
    private long records;        // 总记录数
    private List<?> rows;        // 每行显示的内容
}Getter + Setter
service-api  com/imooc/api/config/InterceptorConfig.java
//拦截器新增地址
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList");

    }
}
service-api  com/imooc/api/interceptors/AdminTokenInterceptor.java
package com.imooc.api.interceptors;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import static com.imooc.api.BaseController.REDIS_ADMIN_TOKEN;

public class AdminTokenInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,在访问controller调用之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String adminUserId = request.getHeader("adminUserId");
        String adminUserToken = request.getHeader("adminUserToken");

        System.out.println("=====================================================================");
        System.out.println("AdminTokenInterceptor - adminUserId = " + adminUserId);
        System.out.println("AdminTokenInterceptor - adminUserToken = " + adminUserToken);
        System.out.println("=====================================================================");

        boolean run = verifyUserIdToken(adminUserId, adminUserToken, REDIS_ADMIN_TOKEN);
        return run;
    }

    /**
     * 请求访问controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求访问controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

账号注销 【admin账号】(删掉redis和cookie数据)

service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.enums.FaceVerifyType;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.FaceVerifyUtils;
import com.imooc.utils.PagedGridResult;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {

    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private RedisOperator redis;

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private FaceVerifyUtils faceVerifyUtils;

    @Override
    public GraceJSONResult adminLogin(AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. 查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2. 判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }

        // 3. 判断密码是否匹配
        boolean isPwdMatch = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMatch) {
            doLoginSettings(admin, request, response);
            return GraceJSONResult.ok();
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }


    /**
     * 用于admin用户登录过后的基本信息设置
     * @param admin
     * @param request
     * @param response
     */
    private void doLoginSettings(AdminUser admin,
                                 HttpServletRequest request,
                                 HttpServletResponse response) {
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(), token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }

    @Override
    public GraceJSONResult adminIsExist(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }

    private void checkAdminExist(String username) {
        AdminUser admin = adminUserService.queryAdminByUsername(username);

        if (admin != null) {
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }
    }

    @Override
    public GraceJSONResult addNewAdmin(NewAdminBO newAdminBO,
                                       HttpServletRequest request,
                                       HttpServletResponse response) {

        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. base64不为空,则代表人脸入库,否则需要用户输入密码和确认密码
        if (StringUtils.isBlank(newAdminBO.getImg64())) {
            if (StringUtils.isBlank(newAdminBO.getPassword()) ||
                    StringUtils.isBlank(newAdminBO.getConfirmPassword())
            ) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_NULL_ERROR);
            }
        }

        // 2. 密码不为空,则必须判断两次输入一致
        if (StringUtils.isNotBlank(newAdminBO.getPassword())) {
            if (!newAdminBO.getPassword()
                    .equalsIgnoreCase(newAdminBO.getConfirmPassword())) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_ERROR);
            }
        }

        // 3. 校验用户名唯一
        checkAdminExist(newAdminBO.getUsername());

        // 4. 调用service存入admin信息
        adminUserService.createAdminUser(newAdminBO);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getAdminList(Integer page, Integer pageSize) {

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult result = adminUserService.queryAdminList(page, pageSize);
        return GraceJSONResult.ok(result);
    }

    @Override
    public GraceJSONResult adminLogout(String adminId,
                                       HttpServletRequest request,
                                       HttpServletResponse response) {

        // 从redis中删除admin的会话token
        redis.del(REDIS_ADMIN_TOKEN + ":" + adminId);

        // 从cookie中清理adming登录的相关信息
        deleteCookie(request, response, "atoken");
        deleteCookie(request, response, "aid");
        deleteCookie(request, response, "aname");

        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护", tags = {"管理员admin维护的controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {

    @ApiOperation(value = "hello方法的接口", notes = "hello方法的接口", httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在", notes = "查询admin用户名是否存在", httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminIsExist(@RequestParam String username);

    @ApiOperation(value = "创建admin", notes = "创建admin", httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,
                                       HttpServletRequest request,
                                       HttpServletResponse response);

    @ApiOperation(value = "查询admin列表", notes = "查询admin列表", httpMethod = "POST")
    @PostMapping("/getAdminList")
    public GraceJSONResult getAdminList(
            @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
            @RequestParam Integer page,
            @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false)
            @RequestParam Integer pageSize);

    @ApiOperation(value = "admin退出登录", notes = "admin退出登录", httpMethod = "POST")
    @PostMapping("/adminLogout")
    public GraceJSONResult adminLogout(@RequestParam String adminId,
                                       HttpServletRequest request,
                                       HttpServletResponse response);
                                        HttpServletResponse response);
}
service-api  com/imooc/api/BaseController.java
public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }

    public void deleteCookie(HttpServletRequest request,HttpServletResponse response,String cookieName){
        try {
            String deleteValue = URLEncoder.encode("", "utf-8");
            setCookieValue(request, response, cookieName, deleteValue, COOKIE_DELETE);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

人脸业务流程图梳理

Chrome开启视频调试模式

在谷歌浏览器中打开【每一次重启电脑都要操作】
chrome://flags/#unsafely-treat-insecure-origin-as-secure
|————————————————————————————————|
| http://admin.imoocnews.com:9090,http://admin.imoocnews.com |
|—— ——————————————————————————————|
http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html
可以获取人脸

MongoDB概念 [人脸数据存储]可以存储JSON数据

  • NoSql 数据库
  • 内存级别查询
  • 不支持事务
  • 非并发读写 请求并发数据量大
  • GridFS 小文件存储

MongoDB术语

数据库 ElasticSearch MongoDB
database es库 database
table表 index索引 collection数据集合
row行 (记录) document文档 (json) document文档 (json)
column 字段列 field域 field域
index索引 - index索引
join表关联查询 - -
pk主键 _id _id
MongoDB数据结构
UserList:[
    {
        userId: "1001",
        username: "lee",
        age: 18
    },
    {
        userId: "1002",
        username: "jay",
        age: 20,
        sex: "boy"
    }
]

----------------------------------------------------------

UserList -->   collection
{}       -->   document
属性        -->   column

MogoDB安装与配置使用

https://www.mongodb.com/try/download/enterprise

将mongodb-linux-x86_64-rhel70-4.2.8传入虚拟机
[imooc@imooc ~]$ tar -zxvf mongodb-linux-x86_64-rhel70-4.2.8.tgz 
[imooc@imooc ~]$ sudo mv mongodb-linux-x86_64-rhel70-4.2.8 /usr/local/mongodb
[imooc@imooc ~]$ cd /usr/local/
[imooc@imooc local]$ ll
drwxrwxr-x. 3 imooc imooc      135 7月  16 19:46 mongodb
[imooc@imooc local]$ cd mongodb/
[imooc@imooc mongodb]$ ll
总用量 312
drwxrwxr-x. 2 imooc imooc    231 7月  16 19:46 bin
-rw-r--r--. 1 imooc imooc  30608 6月  12 2020 LICENSE-Community.txt
-rw-r--r--. 1 imooc imooc  16726 6月  12 2020 MPL-2
-rw-r--r--. 1 imooc imooc   2617 6月  12 2020 README
-rw-r--r--. 1 imooc imooc  75405 6月  12 2020 THIRD-PARTY-NOTICES
-rw-r--r--. 1 imooc imooc 183512 6月  12 2020 THIRD-PARTY-NOTICES.gotools
[imooc@imooc mongodb]$ cd bin/
[imooc@imooc bin]$ pwd
/usr/local/mongodb/bin

[imooc@imooc bin]$ sudo vim /etc/profile
最下面添加:
export JAVA_HOME=/usr/java/jdk1.8.0_222-ea
export CLASSPATH=.:%JAVA_HOME%/lib/dt.jar:%JAVA_HOME%/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin
#set mogodb config
export PATH=/usr/local/mongodb/bin:$PATH

#修改 /etc/profile 文件后,需要重新加载这个文件才能使新配置生效。你可以执行以下命令:
[imooc@imooc bin]$ source /etc/profile

[imooc@imooc bin]$ mongo --version
MongoDB shell version v4.2.8
git version: 43d25964249164d76d5e04dd6cf38f6111e21f5f
OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013
allocator: tcmalloc
modules: none
build environment:
    distmod: rhel70
    distarch: x86_64
    target_arch: x86_64

[imooc@imooc bin]$ cd /usr/local/mongodb/
[imooc@imooc mongodb]$ pwd
/usr/local/mongodb

#创建数据存储目录
[imooc@imooc mongodb]$ mkdir data/db -p #出来一个data
[imooc@imooc mongodb]$ ll
总用量 312
drwxrwxr-x. 2 imooc imooc    231 7月  16 19:46 bin
drwxrwxr-x. 3 imooc imooc     16 7月  16 20:00 data
[imooc@imooc mongodb]$ cd data
[imooc@imooc data]$ ll
总用量 0
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 db
[imooc@imooc data]$ mkdir logs
[imooc@imooc data]$ ll
总用量 0
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 db
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 logs
[imooc@imooc data]$ cd logs/
[imooc@imooc logs]$ pwd
/usr/local/mongodb/data/logs
[imooc@imooc logs]$ touch mongodb.log
[imooc@imooc logs]$ ll
总用量 0
-rw-rw-r--. 1 imooc imooc 0 7月  16 20:01 mongodb.log
[imooc@imooc logs]$ cd ..
[imooc@imooc logs]$ cd ..

[imooc@imooc mongodb]$ vim mongodb.conf
port=27017
# datasource path
dbpath=/user/local/mongodb/data/db
# log path
logpath=/usr/local/mongodb/data/logs/mongodb.log
# append log
logappend=true
# cut useless log
quiet=true
# back desktop auto run
fork=true
# Maxcontect
maxConns=100
# Not open Verify permissions
noauth=true
# open Verify permissions
# auth=true
# open log => true
journal=true
# clash
bind_ip=0.0.0.0

[imooc@imooc mongodb]$ sudo yum install net-snmp
 
#错误:软件包:1:net-snmp-agent-libs-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18(libmysqlclient_18)(64bit)
#错误:软件包:1:net-snmp-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18()(64bit)
#错误:软件包:1:net-snmp-agent-libs-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18()(64bit)
# cd /usr/local/mongodb/
[imooc@imooc mongodb]$ mongod -f mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 4989
child process started successfully, parent exiting

[imooc@imooc mongodb]$ ps aux | grep mongod
imooc      4989  1.8  4.2 1550916 78280 ?       Sl   20:39   0:00 mongod -f mongodb.conf
imooc      5105  0.0  0.0 112824   988 pts/0    S+   20:40   0:00 grep --color=auto mongod

[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      4989      1  0 20:39 ?        00:00:02 mongod -f mongodb.conf
imooc      5201   2948  0 20:44 pts/0    00:00:00 grep --color=auto mongodb


尝试连接到 MongoDB 实例: 
[imooc@imooc mongodb]$ mongo --port 27017

可视化管理工具【MongoDB】

在Navicat里新建链接MongoDB
主机:192.168.170.135
右键新建数据库school → 集合 → 右键新建 左上角保存student
[imooc@imooc mongodb]$ vim mongodb.conf
##### 启用用户账号权限
# Not open Verify permissions
# noauth=true
# open Verify permissions
  auth=true
#重启服务
[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      4989      1  0 20:39 ?        00:00:05 mongod -f mongodb.conf
imooc      5380   2948  0 20:54 pts/0    00:00:00 grep --color=auto mongodb
[imooc@imooc mongodb]$ kill -2 4989
[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      5395   2948  0 20:54 pts/0    00:00:00 grep --color=auto mongodb
[imooc@imooc mongodb]$ mongod -f mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 5419
child process started successfully, parent exiting

[imooc@imooc mongodb]$ mongo
MongoDB shell version v4.2.8
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("c87ffbd9-69cd-4e29-badd-5b86a314f428") }
MongoDB server version: 4.2.8
> use admin
switched to db admin
> db.createUser({user:"root",pwd:"root",roles:["root"]})
Successfully added user: { "user" : "root", "roles" : [ "root" ] }
> db.auth("root","root")
1
> show users
{
    "_id" : "admin.root",
    "userId" : UUID("2ced1f0a-8de4-4fab-9cb8-8e420fe9dcba"),
    "user" : "root",
    "db" : "admin",
    "roles" : [
        {
            "role" : "root",
            "db" : "admin"
        }
    ],
    "mechanisms" : [
        "SCRAM-SHA-1",
        "SCRAM-SHA-256"
    ]
}
> 
#后面关闭连接 编辑数据库 新增密码登录 root root

整合SpringBoot 【GridFS】

<!-- 引入 mongodb 依赖 -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver</artifactId>
        </dependency>
        <dependency>
service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的
    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;
}
service-files  application.yml
  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news

实现人脸入库 【GridFS】

service-files  com/imooc/files/controller/FileUploadController.java

...
 @Autowired
    private GridFSBucket gridFSBucket;
...

 @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO) throws Exception {
        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();
        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());
        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        //上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);
        // 获取文件在gridfs中的主键id
        String fileIdStr = fileId.toString();
        // 下次提交的时候会提交到后端
        return GraceJSONResult.ok(fileIdStr);
    }

http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html
注册并且提交人脸信息
去Navicat → MongoDB → imooc-news → GridFS存储桶 → fs → admin456.png
service-files  com/imooc/files/controller/FileUploadController.java
...
    @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO) throws Exception {
        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();
        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());
        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        //上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);
        // 获取文件在gridfs中的主键id
        String fileIdStr = fileId.toString();
        // 下次提交的时候会提交到后端
        return GraceJSONResult.ok(fileIdStr);
    }
service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;
}
service-files  com/imooc/files/GridFSConfig.java
package com.imooc.files;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component //可以被容器访问到
public class GridFSConfig {
    @Value("${spring.data.mongodb.database}")
    private String mongodb;

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient){
        MongoDatabase mongoDatabase = mongoClient.getDatabase(mongodb);
        GridFSBucket bucket = GridFSBuckets.create(mongoDatabase);//存入mongodatabase
        return bucket;
    }
}

查看admin人脸信息 【GridFS】

service-files  com/imooc/files/controller/FileUploaderController.java
 @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO)
            throws Exception {

        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();

        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());

        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

        // 上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);

        // 获得文件在gridfs中的主键id
        String fileIdStr = fileId.toString();

        return GraceJSONResult.ok(fileIdStr);
    }

    @Override
    public void readInGridFS(String faceId,
                             HttpServletRequest request,
                             HttpServletResponse response) throws Exception {

        // 0. 判断参数
        if (StringUtils.isBlank(faceId) || faceId.equalsIgnoreCase("null")) {
            GraceException.display(ResponseStatusEnum.FILE_NOT_EXIST_ERROR);
        }

        // 1. 从gridfs中读取
        File adminFace = readGridFSByFaceId(faceId);

        // 2. 把人脸图片输出到浏览器
        FileUtils.downloadFileByStream(response, adminFace);
    }

    private File readGridFSByFaceId(String faceId) throws Exception {

        GridFSFindIterable gridFSFiles
                = gridFSBucket.find(Filters.eq("_id", new ObjectId(faceId)));

        GridFSFile gridFS = gridFSFiles.first();

        if (gridFS == null) {
            GraceException.display(ResponseStatusEnum.FILE_NOT_EXIST_ERROR);
        }

        String fileName = gridFS.getFilename();
        System.out.println(fileName);

        // 获取文件流,保存文件到本地或者服务器的临时目录
        File fileTemp = new File("/workspace/temp_face");
        if (!fileTemp.exists()) {
            fileTemp.mkdirs();
        }

        File myFile = new File("/workspace/temp_face/" + fileName);

        // 创建文件输出流
        OutputStream os = new FileOutputStream(myFile);
        // 下载到服务器或者本地
        gridFSBucket.downloadToStream(new ObjectId(faceId), os);

        return myFile;
    }
service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;

    @GetMapping("/readInGridFS")
    public void readInGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;
}

//AdminCookieToken也可以获得faceId

阿里AI人脸识别介绍

人脸人体-阿里云视觉智能开放平台 (aliyun.com)

视觉智能开放平台-控制台 (aliyun.com)

能力展示-阿里云视觉智能开放平台 (aliyun.com)

获得人脸faceId【人脸登录】

service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
@ApiOperation(value = "admin管理员的人脸登录", notes = "admin管理员的人脸登录", httpMethod = "POST")
    @PostMapping("/adminFaceLogin")
    public GraceJSONResult adminFaceLogin(@RequestBody AdminLoginBO adminLoginBO,
                                          HttpServletRequest request,
                                          HttpServletResponse response);
service-admin  com/imooc/admin/controller/AdminMngController.java
 @Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据

        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录

        // 4. admin登录后的数据设置,redis与cookie

        return null;
    }
service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;

    @GetMapping("/readInGridFS")
    public void readInGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;

    /**
     * 从gridfs中读取图片内容 返回base64数据
     * @param faceId
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @GetMapping("/readFace64InGridFS")
    public GraceJSONResult readFace64InGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
service-files  com/imooc/files/controller/FileUploaderController.java
@Override
    public GraceJSONResult readFace64InGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 0. 获得gridfs中人脸文件
        File myface = readGridFSByFaceId(faceId);

        // 1. 转换人脸为base64
        String base64Face = FileUtils.fileToBase64(myface);
        return GraceJSONResult.ok(base64Face);
    }

整合RestTemplate服务通信 【人脸登录】

service-admin  com/imooc/admin/controller/AdminMngController.java
@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {

@Autowired
    private RestTemplate restTemplate;

@Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据
        String fileServerUrlExecute = "http://files.imoocnews.com:8004/fs/readFace64InGridFS?faceId=" + adminFaceId;
        ResponseEntity<GraceJSONResult> responseEntity = restTemplate.getForEntity(fileServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        String base64DB = (String)bodyResult.getData();
        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录

        // 4. admin登录后的数据设置,redis与cookie

        return null;
    }
}
service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

实现人脸对比进行登录【人脸对比】没买人脸识别服务,简单写了一下

service-admin  com/imooc/admin/controller/AdminMngController.java
@Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据
        String fileServerUrlExecute = "http://files.imoocnews.com:8004/fs/readFace64InGridFS?faceId=" + adminFaceId;
        ResponseEntity<GraceJSONResult> responseEntity = restTemplate.getForEntity(fileServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        String base64DB = (String)bodyResult.getData();
        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录
        boolean result = faceVerifyUtils.faceVerify(FaceVerifyType.BASE64.type,
                tempFace64,
                base64DB,
                60);
        if (!result){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_LOGIN_ERROR);
        }
        // 4. admin登录后的数据设置,redis与cookie
        doLoginSettings(admin,request,response);
        return GraceJSONResult.ok();
    }
dev-common  com/imooc/utils/FaceVerifyUtils.java
package com.imooc.utils;

import com.aliyuncs.utils.Base64Helper;
import com.imooc.enums.FaceVerifyType;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.extend.AliyunResource;
import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.SimpleTimeZone;

@Component
public class FaceVerifyUtils {

    final static Logger logger = LoggerFactory.getLogger(FaceVerifyUtils.class);

    @Autowired
    private AliyunResource aliyunResource;

    //网关地址
    private static final String gateway = "https://dtplus-cn-shanghai.data.aliyuncs.com/face/verify";

    /*
     * 计算MD5+BASE64
     */
    public static String MD5Base64(String s) {
        if (s == null)
            return null;
        String encodeStr = "";
        byte[] utfBytes = s.getBytes();
        MessageDigest mdTemp;
        try {
            mdTemp = MessageDigest.getInstance("MD5");
            mdTemp.update(utfBytes);
            byte[] md5Bytes = mdTemp.digest();
            Base64Helper b64Encoder = new Base64Helper();
            encodeStr = b64Encoder.encode(md5Bytes);
        } catch (Exception e) {
            throw new Error("Failed to generate MD5 : " + e.getMessage());
        }
        return encodeStr;
    }

    /*
     * 计算 HMAC-SHA1
     */
    public static String HMACSha1(String data, String key) {
        String result;
        try {
            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(signingKey);
            byte[] rawHmac = mac.doFinal(data.getBytes());
            result = (new Base64Helper()).encode(rawHmac);
        } catch (Exception e) {
            throw new Error("Failed to generate HMAC : " + e.getMessage());
        }
        return result;
    }

    /*
     * 等同于javaScript中的 new Date().toUTCString();
     */
    public static String toGMTString(Date date) {
        SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.UK);
        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
        return df.format(date);
    }

    /**
     * 发送POST请求 进行两张图的人脸对比
     * @param type
     *          0: 通过url识别,参数image_url不为空;1: 通过图片content识别,参数content不为空
     * @param face1
     *          type为0,则传入图片url,为1则传入base64
     * @param face2
     *          type为0,则传入图片url,为1则传入base64
     * @return
     */
    //如果发送的是转换为base64编码后后面加请求参数type为1,如果请求的是图片的url则不用加type参数。
    public String sendPostVerifyFace(int type, String face1, String face2) throws Exception {
        String body = "";
        if (type == FaceVerifyType.BASE64.type) {
            body = "{\"content_1\": \"" + face1 + "\", \"content_2\":\"" + face2 + "\", \"type\":\"" + type + "\"}";
        } else if (type == FaceVerifyType.IMAGE_URL.type) {
            body = "{\"image_url_1\": \"" + face1 + "\", \"image_url_2\":\"" + face2 + "\", \"type\":\"" + type + "\"}";
        } else {
            GraceException.display(ResponseStatusEnum.FACE_VERIFY_TYPE_ERROR);
        }
//        String body = "{\"content_1\": \"" + face1 + "\", \"content_2\":\"" + face2 + "\", \"type\":\"" + "1" + "\"}";
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        int statusCode = 200;
        try {
            URL realUrl = new URL(gateway);
            /*
             * http header 参数
             */
            String method = "POST";
            // 返回值类型
            String accept = "application/json";
            // 请求内容类型
            String content_type = "application/json";
            String path = realUrl.getFile();
            // GMT时间
            String date = toGMTString(new Date());
            // 1.对body做MD5+BASE64加密
            String bodyMd5 = MD5Base64(body);
            String stringToSign = method + "\n" + accept + "\n" + bodyMd5 + "\n" + content_type + "\n" + date + "\n"
                    + path;
            // 2.计算 HMAC-SHA1
            String signature = HMACSha1(stringToSign, aliyunResource.getAccessKeySecret());
            // 3.得到 authorization header
            String authHeader = "Dataplus " + aliyunResource.getAccessKeyID() + ":" + signature;
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("Accept", accept);
            conn.setRequestProperty("Content-type", content_type);
            conn.setRequestProperty("Date", date);
            // 认证信息
            conn.setRequestProperty("Authorization", authHeader);
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(body);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            statusCode = ((HttpURLConnection) conn).getResponseCode();
            if (statusCode != 200) {
                in = new BufferedReader(new InputStreamReader(((HttpURLConnection) conn).getErrorStream()));
            } else {
                in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            }
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        if (statusCode != 200) {
            throw new IOException("\nHttp StatusCode: " + statusCode + "\nErrorMessage: " + result);
        }
        return result;
    }

    /**
     *
     * @param type
     * @param face1
     * @param face2
     * @param targetConfidence
     *          目标可信度,自定义阈值
     * @return
     */
    public boolean faceVerify(int type, String face1, String face2, double targetConfidence) {

        String response = null;
        try {
            response = sendPostVerifyFace(type, face1, face2);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Map<String, String> map = JsonUtils.jsonToPojo(response, Map.class);
        Object confidenceStr = map.get("confidence");
        Double responseConfidence = (Double)confidenceStr;

        logger.info("人脸对比结果:{}", responseConfidence);

//        System.out.println(response.toString());
//        System.out.println(map.toString());

        if (responseConfidence > targetConfidence) {
            return true;
        } else {
            return false;
        }
    }

    /**
     *
     * 将图片转换为Base64
     * 将base64编码字符串解码成img图片
     * @param imgUrl
     * @return
     */
    public String getImgBase64(String imgUrl){
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        try {
            // 创建URL
            URL url = new URL(imgUrl);
            byte[] by = new byte[1024];
            // 创建链接
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            InputStream is = conn.getInputStream();
            // 将内容放到内存中
            int len = -1;
            while ((len = is.read(by)) != -1) {
                data.write(by, 0, len);
            }
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 对字节数组Base64编码
        return Base64.encodeBase64String(data.toByteArray());
    }

//    public static void main(String[] args) {
//        String face3 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF5MvvGAfnLXAAIHiv37wNk363.jpg";
//        String face4 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF5Mv3yAH74mAACOiTd9pO4462.jpg";
//
//        boolean result = new FaceVerifyUtils().faceVerify(FaceVerifyType.IMAGE_URL.type, face3, face4, 60);
//
//        logger.info("人脸对比是否成功:{}", result);
//    }
}

MongoDB使用场景 【分担数据库的大数据量】

  • GridFS小文件存储
  • 历史数据快照 [买的东西涨价后 还是原来的价格] 【数据量大存入MongoDB】
  • 用户浏览记录
  • 客服聊天记录 [不是核心数据 可以剥离]
这些不建议放在Redis里 因为Redis是存储在内存里的 [内存很贵 成本很大]

友情连接保存与更新 【MongoDB】

对连接的一些逻辑校验
service-admin  application.yml 【加上mongodb配置】
  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
service-admin  Application 【注释exclude 把mongodb配置进来】
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication  //(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.admin.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

dev-model com/imooc/pojo/bo/SaveFriendLinkBO.java
package com.imooc.pojo.bo;

import com.imooc.validate.CheckUrl;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class SaveFriendLinkBO {
private String id;
@NotBlank(message = “友情链接名不能为空”)
private String linkName;
@NotBlank(message = “友情链接地址不能为空”)
@CheckUrl 【ctrl+左键 显示↓ CheckUrl接口】
@CheckName 【 //不能有空格 不能为空 字符串长度要在6-12位】
private String linkUrl;
@NotNull(message = “请选择保留或删除”)
private Integer isDelete;
}Getter+Setter

dev-model  com/imooc/validate/CheckUrl.java
package com.imooc.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckUrlValidate.class)
public @interface CheckUrl {

    String message() default "Url不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
dev-model  com/imooc/validate/CheckName.java
package com.imooc.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckUrlValidate.class)
public @interface CheckName {

    String message() default "Name不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
dev-model  com/imooc/validate/CheckUrlValidate.java
package com.imooc.validate;

import com.imooc.utils.UrlUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckUrlValidate implements ConstraintValidator<CheckUrl, String> {

    @Override
    public boolean isValid(String url, ConstraintValidatorContext context) {
        return UrlUtil.verifyUrl(url.trim());
    }
}
dev-model  com/imooc/validate/CheckNameValidate.java
package com.imooc.validate;

import com.imooc.utils.UrlUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckNameValidate implements ConstraintValidator<CheckName, String> {

    @Override
    public boolean isValid(String name, ConstraintValidatorContext context) {
        return UrlUtil.verifyName(name.trim());
    }
}
dev-common  com/imooc/utils/UrlUtil.java 【Url+Name校验标准】
package com.imooc.utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UrlUtil {

    /**
     * 验证是否是URL
     * @param url
     * @return
     */
    public static boolean verifyUrl(String url){

        // URL验证规则
//        String regEx ="[A-Za-z]+://[A-Za-z0-9-_]+\\\\.[A-Za-z0-9-_%&\\?\\/.=]+";
        String regEx = "^([hH][tT]{2}[pP]:/*|[hH][tT]{2}[pP][sS]:/*|[fF][tT][pP]:/*)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\/])+(\\?{0,1}(([A-Za-z0-9-~]+\\={0,1})([A-Za-z0-9-~]*)\\&{0,1})*)$";
        // 编译正则表达式
        Pattern pattern = Pattern.compile(regEx);
        // 忽略大小写的写法
        // Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(url);
        // 字符串是否与正则表达式相匹配
        boolean rs = matcher.matches();
        return rs;

    }
    //不能有空格 不能为空 字符串长度要在6-12位
    public static boolean verifyName(String name){
        // Name验证规则
        String nameEx = "^[^\\s]{6,12}$";
        // 编译正则表达式
        Pattern pattern = Pattern.compile(nameEx);
        // 忽略大小写的写法
        // Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(name);
        // 字符串是否与正则表达式相匹配
        boolean rs = matcher.matches();
        return rs;
    }

    public static void main(String[] args) {
        boolean res = verifyUrl("http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html");
        boolean nres = verifyName("Jerry");
        System.out.println(nres);
    }
}
dev-model  pom.xml
 <!-- 引入 mongodb 依赖 --> 【springboot整合mongodb】
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
真正的友链保存接口
service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
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.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);


    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());
        return GraceJSONResult.ok();
    }
}
dev-model  com/imooc/pojo/mo/FriendLinkMO.java
//这些都是设置到MongoDB数据库的名字
//@Document("FriendLink") //MongoDB文件起别名
public class FriendLinkMO {
    @Id //作为MongDB的主键了
    private String id;
    @Field("link_name")
    private String linkName;
    @Field("link_url")
    private String linkUrl;
    @Field("is_delete")
    private Integer isDelete;
    @Field("create_time")
    private Date createTime;
    @Field("update_time")
    private Date updateTime;
}Getter + Setter

Repository持久层操作保存记录

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
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.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);
    @Autowired
    private FriendLinkService friendLinkService;

    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());

        friendLinkService.saveOrUpdateFriendLink(friendLinkMO);
        return GraceJSONResult.ok();
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html
/* 友情连接 → 
链接名称:慕课网
链接地址:www.imooc.com
[新增/添加]

打开检查→Console
{"status":200,"msg":"操作成功!","success":true,"data":null}

打开数据库查看MongoDB→friendLinkMO有存入的数据即操作成功
*/
service-admin  com/imooc/admin/service/FriendLinkService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;

public interface FriendLinkService {
    /**
     * 新增或者更新友情链接
     */
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO);
}
service-admin  com/imooc/admin/service/impl/FriendLinkServiceImpl.java
package com.imooc.admin.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.AdminUserService;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class FriendLinkServiceImpl implements FriendLinkService {

    @Autowired
    private FriendLinkRepository friendLinkRepository;
    @Override
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO) {
        friendLinkRepository.save(friendLinkMO); //有id更新 无id直接保存
    }
}
service-admin  com/imooc/admin/repository/FriendLinkRepository.java
package com.imooc.admin.repository;

import com.imooc.pojo.mo.FriendLinkMO;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface FriendLinkRepository extends MongoRepository<FriendLinkMO, String> { //持久层
    // 内置提供了很多方法 find.. delete...
}

友情链接查询列表 【MongoDB】

@Document(“FriendLink”) //文件起别名 记得要在MongoDB里面找这个 下面搜索的都在这个文件里面
service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
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.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
    @ApiOperation(value = "查询改友情连接列表", notes = "查询改友情连接列表", httpMethod = "POST")
    @PostMapping("/getFriendLinkList")
    public GraceJSONResult getFriendLinkList();
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);
    @Autowired
    private FriendLinkService friendLinkService;

    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());

        friendLinkService.saveOrUpdateFriendLink(friendLinkMO);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getFriendLinkList() { 
//【用了FriendLinkRepository里面的】extends MongoRepository 中的简单增删改查 
// 里面的删除是逻辑删除
        return GraceJSONResult.ok(friendLinkService.queryAllFriendLinkList());
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html
service-admin  com/imooc/admin/service/FriendLinkService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;

import java.util.List;

public interface FriendLinkService {
    /**
     * 新增或者更新友情链接
     */
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO);

    /**
     * 查询友情链接
     */
    public List<FriendLinkMO> queryAllFriendLinkList();

}
service-admin  com/imooc/admin/service/impl/FriendLinkServiceImpl.java
package com.imooc.admin.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.AdminUserService;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class FriendLinkServiceImpl implements FriendLinkService {

    @Autowired
    private FriendLinkRepository friendLinkRepository;
    @Override
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO) {
        friendLinkRepository.save(friendLinkMO); //有id更新 无id直接保存
    }

    @Override
    public List<FriendLinkMO> queryAllFriendLinkList() {
//        Pageable pageable = PageRequest.of(1,10);
//        friendLinkRepository.findAll(pageable);
        return friendLinkRepository.findAll();
    }
}

友情链接删除 【MongoDB】[增加真实删除]

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
...
/*
@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
    @ApiOperation(value = "查询改友情连接列表", notes = "查询改友情连接列表", httpMethod = "POST")
    @PostMapping("/getFriendLinkList")
    public GraceJSONResult getFriendLinkList();
*/
    @ApiOperation(value = "删除改友情连接列表", notes = "删除改友情连接列表", httpMethod = "POST")
    @PostMapping("/delete")
    public GraceJSONResult delete(@RequestParam String linkId);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
@Override
    public GraceJSONResult delete(String linkId) {
        friendLinkService.delete(linkId);
        return GraceJSONResult.ok();
    }
service-admin  com/imooc/admin/service/FriendLinkService.java
    /**
     * 删除友情链接
     */
    public void delete(String linkId);
service-admin  com/imooc/admin/service/AdminUserService.java
@Override
    public void delete(String linkId) {
        friendLinkRepository.deleteById(linkId);
    }
service-api  com/imooc/api/config/InterceptorConfig.java 【增加友链拦截器】
/*
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
*/
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete");
    }
}

【作业】文章分类管理 [新增或修改分类、查询分类列表、用户端查询分类列表]

http://admin.imoocnews.com:9090/imooc-news/admin/categoryMng.html

@RequestBody的使用-CSDN博客

service-api  com/imooc/api/controller/admin/CategoryMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Api(value = "文章分类维护", tags = {"文章分类维护controller"})
@RequestMapping("categoryMng")
public interface CategoryMngControllerApi {
    @PostMapping("saveOrUpdateCategory")
    @ApiOperation(value = "新增或修改分类", notes = "新增或修改分类", httpMethod = "POST")
    public GraceJSONResult saveOrUpdateCategory(@RequestBody @Valid SaveCatrgoryBO saveCatrgoryBO,
                                                BindingResult result);
    @PostMapping("getCatList")
    @ApiOperation(value = "查询分类列表", notes = "查询分类列表", httpMethod = "POST")
    public GraceJSONResult getCatList();

    @GetMapping("getCats")
    @ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
    public GraceJSONResult getCats();
}
service-admin  com/imooc/admin/controller/CategoryMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.CategoryService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.CategoryMngControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import com.imooc.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryMngController extends BaseController implements CategoryMngControllerApi {

    final static Logger logger = LoggerFactory.getLogger(CategoryMngController.class);

    @Autowired
    private CategoryService categoryService;

    @Override
    public GraceJSONResult saveOrUpdateCategory(SaveCatrgoryBO saveCatrgoryBO, BindingResult result) {
        if (result.hasErrors()){
        // 判断BindingResult是否保存错误的验证信息,如果有,则直接return
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }
        Category newCat = new Category();
        BeanUtils.copyProperties(saveCatrgoryBO,newCat);
        // id为空新增,不为空修改
        if (saveCatrgoryBO.getId() == null){
            //查询新增的分类名称不能重复存在
            boolean isExist = categoryService.queryCatIsExist(newCat.getName(), null);
            if (!isExist){
                //新增到数据库
                categoryService.createCategory(newCat);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.CATEGORY_EXIST_ERROR);
            }
        }else {
            //查询修改的分类名称不能重复存在
            boolean isExist = categoryService.queryCatIsExist(newCat.getName(), saveCatrgoryBO.getOldName());
            if (!isExist){
                //修改到数据库
                categoryService.modifyCategory(newCat);
            } else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.CATEGORY_EXIST_ERROR);
            }
        }
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getCatList() {
        List<Category> categoryList = categoryService.queryCategoryList();
        return GraceJSONResult.ok(categoryList);
    }

    @Override
    public GraceJSONResult getCats() {
        // 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);

        List<Category> categoryList = null;
        if (StringUtils.isBlank(allCatJson)) {
            categoryList = categoryService.queryCategoryList();
            redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
        } else {
            categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
        }

        return GraceJSONResult.ok(categoryList);
    }
}
service-admin  com/imooc/admin/service/CategoryService.java
package com.imooc.admin.service;

import com.imooc.pojo.Category;
import java.util.List;

public interface CategoryService {

    /**
     * 新增文章分类
     */
    public void createCategory(Category category);

    /**
     * 修改文章分类列表
     */
    public void modifyCategory(Category category);

    /**
     * 查询分类名是否已经存在
     */
    public boolean queryCatIsExist(String catName, String oldCatName);

    /**
     * 获得文章分类列表
     */
    public List<Category> queryCategoryList();
}
service-admin  com/imooc/admin/service/impl/CategoryServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.CategoryMapper;
import com.imooc.admin.service.CategoryService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Category;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.List;

import static com.imooc.api.BaseController.REDIS_ALL_CATEGORY;


@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    public CategoryMapper categoryMapper;
    @Autowired
    public RedisOperator redis;

    @Transactional
    @Override
    public void createCategory(Category category) {
// 分类不会很多,所以id不需要自增,这个表的数据也不会多到几万甚至分表,数据都会集中在一起
        int result = categoryMapper.insert(category);
        if (result != 1){
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
            /**
             * 不建议如下做法:
             * 1. 查询redis中的categoryList
             * 2. 转化categoryList为list类型
             * 3. 在categoryList中add一个当前的category
             * 4. 再次转换categoryList为json,并存入redis中
             */
            // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
            redis.del(REDIS_ALL_CATEGORY);
        }
    }

    @Transactional
    @Override
    public void modifyCategory(Category category) {
        int result = categoryMapper.updateByPrimaryKey(category);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
        // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
        redis.del(REDIS_ALL_CATEGORY);
    }

    @Override
    public boolean queryCatIsExist(String catName, String oldCatName) {
        Example example = new Example(Category.class);
        Example.Criteria catCriteria = example.createCriteria();
        catCriteria.andEqualTo("name", catName);
        if (StringUtils.isNotBlank(oldCatName)) {
            catCriteria.andNotEqualTo("name", oldCatName);
        }

        List<Category> catList = categoryMapper.selectByExample(example);

        boolean isExist = false;
        if (catList != null && !catList.isEmpty() && catList.size() > 0) {
            isExist = true;
        }

        return isExist;
    }

    @Override
    public List<Category> queryCategoryList() {
        return categoryMapper.selectAll();
    }
}
service-admin  com/imooc/admin/mapper/CategoryMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Category;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryMapper extends MyMapper<Category> {
}
service-admin  resources/mapper/CategoryMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.CategoryMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Category" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="tag_color" property="tagColor" jdbcType="VARCHAR" />
  </resultMap>
</mapper>
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写
    public static final String REDIS_ADMIN_TOKEN = "redis_admin_token";//ctrl+shift+u直接大写
    public static final String REDIS_ALL_CATEGORY = "redis_all_category";

    public static final String REDIS_WRITER_FANS_COUNTS = "redis_writer_fans_counts";
    public static final String REDIS_MY_FOLLOW_COUNTS = "redis_my_follow_counts";

    public static final String REDIS_ARTICLE_READ_COUNTS = "redis_article_read_counts";
    public static final String REDIS_ALREADY_READ = "redis_already_read";

    public static final String REDIS_ARTICLE_COMMENT_COUNTS = "redis_article_comment_counts";

    @Value("${website.domain-name}")
    public String DOMAIN_NAME;
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
    public static final Integer COOKIE_DELETE = 0;

    public static final Integer COMMON_START_PAGE = 1;
    public static final Integer COMMON_PAGE_SIZE = 10;
}...
dev-model  com/imooc/pojo/Category.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;

public class Category {
    @Id
    private Integer id;

    /**
     * 分类名,比如:科技,人文,历史,汽车等等
     */
    private String name;

    /**
     * 标签颜色
     */
    @Column(name = "tag_color")
    private String tagColor;
}Getter + Setter

查询用户列表_设置时间日期转换配置 【用户管理】

service-api  com/imooc/api/controller/user/AppUserMngControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Date;

@Api(value = "用户管理相关的接口定义",tags = {"用户管理相关功能的controller"})
@RequestMapping("appUser")
public interface AppUserMngControllerApi {
    @ApiOperation(value = "查询所有网站用户",notes = "查询所有网站用户",httpMethod = "POST")
    @PostMapping("queryAll")
    public GraceJSONResult queryAll(@RequestParam String nickname,
                                    @RequestParam Integer status,
                                    @RequestParam Date startDate,
                                    @RequestParam Date endDate,
                                    @RequestParam Integer page,
                                    @RequestParam Integer pageSize);
}
service-user  com/imooc/user/controller/AppUserMngController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.AppUserMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
public class AppUserMngController extends BaseController implements AppUserMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AppUserMngController.class);
// 字符串无法直接转换成Date类型 需要工具类转换 DateConverterConfig com/imooc/api/config/DateConverterConfig.java

    @Override
    public GraceJSONResult queryAll(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        System.out.println(startDate);
        System.out.println(endDate);
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }

        return GraceJSONResult.ok();
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/userList.html
service-api  com/imooc/api/config/DateConverterConfig.java
package com.imooc.api.config;


import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 请求路径url中的参数进行时间日期类型的转换,字符串->日期Date
 */
@Configuration
public class DateConverterConfig implements Converter<String, Date> {

    private static final List<String> formatterList = new ArrayList<>(4);
    static{
        formatterList.add("yyyy-MM");
        formatterList.add("yyyy-MM-dd");
        formatterList.add("yyyy-MM-dd hh:mm");
        formatterList.add("yyyy-MM-dd hh:mm:ss");
    }

    @Override
    public Date convert(String source) {
        String value = source.trim();
        if ("".equals(value)) {
            return null;
        }
        if(source.matches("^\\d{4}-\\d{1,2}$")){
            return parseDate(source, formatterList.get(0));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")){
            return parseDate(source, formatterList.get(1));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formatterList.get(2));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formatterList.get(3));
        }else {
            GraceException.display(ResponseStatusEnum.SYSTEM_DATE_PARSER_ERROR);
        }
        return null;
    }

    /**
     * 日期转换方法
     * @param dateStr
     * @param formatter
     * @return
     */
    public Date parseDate(String dateStr, String formatter) {
        Date date=null;
        try {
            DateFormat dateFormat = new SimpleDateFormat(formatter);
            date = dateFormat.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return date;
    }
}

查询用户列表_实现service与联调 【用户管理】

service-user  com/imooc/user/service/AppUserMngService.java
package com.imooc.user.service;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface AppUserMngService {
    /**
     * 查询管理员列表
     * @param nickname
     * @param status
     * @param startDate
     * @param endDate
     * @param page
     * @param pageSize
     * @return
     */
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

}
service-api  com/imooc/api/service/BaseService.java
package com.imooc.api.service;

import com.github.pagehelper.PageInfo;
import com.imooc.utils.PagedGridResult;

import java.util.List;

public class BaseService {
    public PagedGridResult setterPagedGrid(List<?> list, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(list);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(list);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getTotal());
        gridResult.setTotal(pageList.getPages());
        return gridResult;

    }
}
service-user  com/imooc/user/service/impl/AppUserMngServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.UserService;
import com.imooc.utils.*;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class AppUserMngServiceImpl extends BaseService implements AppUserMngService {
    @Autowired
    public AppUserMapper appUserMapper;

    @Override
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        Example example = new Example(AppUser.class);
        example.orderBy("createdTime").desc();
        Example.Criteria criteria = example.createCriteria();
        if (StringUtils.isNotBlank(nickname)) {
            criteria.andLike("nickname", "%" + nickname + "%");
        }
        if (UserStatus.isUserStatusValid(status)){
            criteria.andEqualTo("activeStatus", status); //对比状态
        }
        if (startDate != null){
            criteria.andGreaterThanOrEqualTo("createdTime", startDate);//数据库和传入参数对比
        }
        if (endDate != null){
            criteria.andLessThanOrEqualTo("endTime", endDate);//数据库和传入参数对比
        }
        PageHelper.startPage(page, pageSize);
        List<AppUser> list = appUserMapper.selectByExample(example);

        return setterPagedGrid(list,page);
    }
}

查询用户账户_冻结与解封 【用户管理】

service-api  com/imooc/api/controller/user/AppUserMngControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Date;

@Api(value = "用户管理相关的接口定义",tags = {"用户管理相关功能的controller"})
@RequestMapping("appUser")
public interface AppUserMngControllerApi {
    @ApiOperation(value = "查询所有网站用户",notes = "查询所有网站用户",httpMethod = "POST")
    @PostMapping("queryAll")
    public GraceJSONResult queryAll(@RequestParam String nickname,
                                    @RequestParam Integer status,
                                    @RequestParam Date startDate,
                                    @RequestParam Date endDate,
                                    @RequestParam Integer page,
                                    @RequestParam Integer pageSize);

    @ApiOperation(value = "查看用户详情",notes = "查看用户详情",httpMethod = "POST")
    @PostMapping("userDetail")
    public GraceJSONResult userDetail(@RequestParam String userId);

    @ApiOperation(value = "冻结用户或者解冻用户",notes = "冻结用户或者解冻用户",httpMethod = "POST")
    @PostMapping("freezeUserOrNot")
    public GraceJSONResult freezeUserOrNot(@RequestParam String userId,@RequestParam Integer doStatus);
}
service-user  com/imooc/user/controller/AppUserMngController.java
@RestController
public class AppUserMngController extends BaseController implements AppUserMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AppUserMngController.class);
    
    @Autowired
    private AppUserMngService appUserMngService;
    @Autowired
    private UserService userService;
......
    
     @Override
    public GraceJSONResult freezeUserOrNot(String userId, Integer doStatus) {
        if (!UserStatus.isUserStatusValid(doStatus)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_STATUS_ERROR);
        }
        appUserMngService.freezeUserOrNot(userId, doStatus);
        //若冻结后 用户处于登录状态 还可以进行操作 所以要刷新用户状态
        //方法①:删除用户会话,从而保证用户需要重新登陆以后再来刷新她的会话状态
        redis.del(REDIS_USER_INFO + ":" + userId);
        //方法②:查询最新用户的信息,重新放入redis中,做一次更新
        return GraceJSONResult.ok();
    }
service-user  com/imooc/user/service/AppUserMngService.java
package com.imooc.user.service;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface AppUserMngService {
    /**
     * 查询管理员列表
     */
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

    /**
     * 冻结用户账号或者解除冻结
     */
    public void freezeUserOrNot(String userId, Integer doStatus);

}
service-user  com/imooc/user/service/impl/AppUserMngServiceImpl.java
    @Transactional
    @Override
    public void freezeUserOrNot(String userId, Integer doStatus) {
        AppUser user = new AppUser();
        user.setId(userId);
        user.setActiveStatus(doStatus);
        appUserMapper.updateByPrimaryKeySelective(user);
    }

梳理文章article表结构 【文章服务】

  • 构建文章服务
  • 作者中心发表文章
  • 作者中心内容管理
  • 自动审核[阿里客户端],手动审核

构建文章服务工程 【文章服务】

新创建一个Module
GroupId:com.imooc
ArtifactId:imooc-news-dev-service-article
pom参考service-admin移植 resources里的所有文件(除mapper)也要移植

service-article  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-article</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
resources logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-article"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-article"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-article.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-article  com/imooc/article/Application.java
package com.imooc.article;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-article  com/imooc/article/controller/HelloController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}

========================================================================
http://localhost:8001/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}
service-article  application-dev
server:
  port: 8001

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
## setup CN from java, This is resource
website:
  domain-name: imoocnews.com

## open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
############################################################
#
# admin用户微服务
# web访问端口号  约定:8001
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-article
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: org.mariadb.jdbc.Driver       # mysql驱动
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&sessionVariables=tx_isolation='READ-COMMITTED'
    username: root
    password: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.imooc.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.imooc.my.mapper.MyMapper
  not-empty: false    # 在进行数据库操作的的时候,判断表达式 username != null, 是否追加 username != ''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true

summernote与多文件上传需求 【发头条】

https://summernote.org/

【前端工程里面的】createArticle.html
...
<script src="libs/vue.min.js"></script>
<script src="libs/axios.min.js"></script>

<link href="./libs/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="./libs/jquery-3.4.1.min.js"></script>
<script src="libs/layDate-v5.0.9/laydate/laydate.js"></script>
<script src="./libs/bootstrap/js/bootstrap.min.js"></script>
<link href="./libs/summernote/dist/summernote.css" rel="stylesheet">
<script src="./libs/summernote/dist/summernote.js"></script>
<!-- 中文汉化 -->
<script src="libs/summernote/lang/summernote-zh-CN.js"></script>
<script src="js/app.js"></script>

<script type="text/javascript">

......
<!-- 富文本编辑器 -->
        <div id="editor2" class="editor-container">

            <div class="article-title-wrapper">
                <input id="title" class="article-title" placeholder="请输入文字标题(6-30长度)" v-model="articleTitle" maxlength="30"/>
            </div>

            <div class="article-content-wrapper">
                <div id="summernote" class="summernote"></div>
            </div>

            <div class="other-info">
                <div class="cover-wrapper">
                    <div class="cover">文章领域</div>
                    <div class="choose-type">
                        <!-- <select v-model="articleCategory">
                            <option value="0">请选择</option>
                            <option value="1">汽车</option>
                            <option value="2">科技</option>
                            <option value="3">历史</option>
                        </select> -->

                        <select v-model="articleCategory">
                            <option :value="cat.id" v-for="(cat, index) in catList" v-key="index">{{cat.name}}</option>
                        </select>
                        
                    </div>
                </div>
                <div class="cover-wrapper">
                    <div class="cover">文章封面</div>
                    <div class="choose-type">
                        <div><input type="radio" name="articleType" v-model="articleType" value="1" checked/><span class="choose-words">单封面</span></div>
                        <div style="margin-left: 30px;"><input type="radio" v-model="articleType" value="2" name="articleType"/><span class="choose-words">无封面</span></div>
                    </div>
                </div>
                <div class="cover-wrapper" v-show="articleType==1">
                    <div class="cover"></div>
                    <div class="choose-cover">
                        <div class="uploader-comp">
                            <div id="block-choose" class="block-choose" :style="coverStyle">
                                <img src="./img/icon-go-upload.png" style="width: 20px; height: 20px; align-self: center;" v-show="articleCover == '' || articleCover == null"/>
                            </div>
                            <input type="file" @change="uploadCover" @mouseover="mouseOver" @mouseout="mouseOut" id="inputPic" class="inputPic" accept="image/jpeg,image/jpg,image/png">
                        </div>
                        <div style="margin-top: 10px; color: #9b9d9e;">请上传JPG、JPEG、PNG格式的封面图噢~</div>
                    </div>
                </div>
            </div>

            <div class="publish-bottom">
                <div class="buttons">
                    <button class="white-btn" type="button" @click="goBack">返回</button>
                    <button class="white-btn" type="button" @click="preview">预览</button>
                    <!-- <button class="white-btn" type="button" @click="save">保存草稿</button> -->
                    <!-- FIXME: 计算剩余时间,使用RMQ延时队列,或分布式定时任务 -->
                    <button class="white-btn" type="button" @click="doTiming">{{appointWords}}</button>
                    
                    <input type="text" class="timing-date-picker" placeholder="定时日期" id="choose-date" v-show="isAppoint==1" readonly>

                    <button class="red-btn" type="button" @click="publish">发布文章</button>
                </div>
            </div>
        </div>
    </div>

......
// 初始化编辑器
            $('#summernote').summernote({
                placeholder: '请输入正文...',
                lang: 'zh-CN',
                height: 600,
                width: 800,
                border: 0,
                // disableDragAndDrop: true, // 禁止文件拖放
                toolbar: [
                    ['style', ['style']],
                    ['font', ['bold', 'underline', 'clear']],
                    ['color', ['color']],
                    ['para', ['ul', 'ol', 'paragraph']],
                    ['table', ['table']],
                    ['insert', ['link', 'picture']],
                    ['view', ['fullscreen', 'codeview', 'help']]
                ],

实现多文件上传uploadSomeFiles 【发头条】

媒体号作家中心 | 发文章 (imoocnews.com)

service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    /**
     * 上传单文件
     * @param userId
     * @param file
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "上传用户头像",notes = "上传用户头像",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    /**
     * 上传多文件
     * @param userId
     * @param files
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "上传用户头像",notes = "上传用户头像",httpMethod = "POST")
    @PostMapping("/uploadSomeFiles")  //因为前端createArticle.html 178行 multiForm.append('files',f,f.name);
    public GraceJSONResult uploadSomeFiles(@RequestParam String userId, MultipartFile[] files) throws Exception;
......
}
service-file  com/imooc/files/controller/FileUploaderController.java
......
@Override
    public GraceJSONResult uploadSomeFiles(String userId, MultipartFile[] files) throws Exception {
        // 声明一个list,用于存放多个图片的地址路径,返回到前端
        List<String> imageUrlList = new ArrayList<>();
        if (files != null && files.length > 0){
            for (MultipartFile file: files){
                String path = "";
                if (file != null){
                    // 获得文件上传的名称
                    String fileName = file.getOriginalFilename();
                    //判断文件名不能为空
                    if (StringUtils.isNotBlank(fileName)){
                        String fileNameArr[] = fileName.split("\\.");
                        //获得后缀名
                        String suffix = fileNameArr[fileNameArr.length - 1];
                        //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                        if (!suffix.equalsIgnoreCase("png") &&
                                !suffix.equalsIgnoreCase("jpg") &&
                                !suffix.equalsIgnoreCase("jpeg")
                        ){
                           continue;
                        }
                        // fdfs执行上传     要让外面得以访问 ①需要把内网的环境发布到公网 [内网穿透]  ②路由器端口映射到外网  ③fastdfs安装到公网里
                        // path = uploaderService.uploadFdfs(file, suffix);
                        // OSS执行上传
                        path = uploaderService.uploadOSS(file, userId, suffix);
                    }else {
                        continue;
                    }
                }else {
                    continue;
                }
                String finalPath = "";
                if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
                    finalPath = fileResource.getOssHost() + path;
                    // FIXME: 放入到imageUrlList之前,需要对图片做一次审核 [doAliImageReview]
                    imageUrlList.add(finalPath);
                }  else{
                    continue;
                }
//                return GraceJSONResult.ok(finalPath);
//        return GraceJSONResult.ok(doAliImageReview(finalPath)); //这里加了图片审核咯
            }
        }
        return GraceJSONResult.ok(imageUrlList);
    }
......
service-api  com/imooc/api/config/InterceptorConfig.java 
package com.imooc.api.config;
//【增加拦截uploadSomeFiles】
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace")
                .addPathPatterns("/fs/uploadSomeFiles");

        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete")
                .addPathPatterns("/categoryMng/saveOrUpdateCategory")
                .addPathPatterns("/categoryMng/getCatList");

        registry.addInterceptor(userActiveInterceptor())
                .addPathPatterns("/fs/uploadSomeFiles");
    }
}

获得列表_业务接口解耦与Redis缓存应用 【文章领域】

getCatList 和 getCats 一个是用户端一个是admin 业务体系不一样 所以同样是查询分类列表
但是还是应该拆开 使耦合减少 得到高效解耦
查询放在Redis里面 效率变高

媒体号作家中心 | 发文章 (imoocnews.com)
刷新一下 文章领域就可以找到那些分类
Redis里面会有信息 redis_all_category
[{“id”:2,”name”:”汽车”,”tagColor”:”#8939bd”},{“id”:3,”name”:”娱乐”,”tagColor”:”#c939aa”},{“id”:5,”name”:”地理”,”tagColor”:”#57394a”},{“id”:6,”name”:”历史”,”tagColor”:”#29ab4a”},{“id”:7,”name”:”科技”,”tagColor”:”#2467bc”},{“id”:9,”name”:”体育”,”tagColor”:”#c98f4a”},{“id”:10,”name”:”搞笑”,”tagColor”:”#68b84a”},{“id”:11,”name”:”技术”,”tagColor”:”#c9394a”},{“id”:12,”name”:”慕课”,”tagColor”:”#682aa8”},{“id”:13,”name”:”技能”,”tagColor”:”#c9394a”},{“id”:14,”name”:”课网”,”tagColor”:”#c9a24a”}]

service-api  com/imooc/api/controller/admin/CategoryMngControllerApi.java
// 【getCasts】
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Api(value = "文章分类维护", tags = {"文章分类维护controller"})
@RequestMapping("categoryMng")
public interface CategoryMngControllerApi {
    @PostMapping("saveOrUpdateCategory")
    @ApiOperation(value = "新增或修改分类", notes = "新增或修改分类", httpMethod = "POST")
    public GraceJSONResult saveOrUpdateCategory(@RequestBody @Valid SaveCatrgoryBO saveCatrgoryBO,
                                                BindingResult result);
    @PostMapping("getCatList")
    @ApiOperation(value = "查询分类列表", notes = "查询分类列表", httpMethod = "POST")
    public GraceJSONResult getCatList();

    @GetMapping("getCats")
    @ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
    public GraceJSONResult getCats();
}
service-admin  com/imooc/admin/controller/CategoryMngController.java
@Override
    public GraceJSONResult getCats() {
        // 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);

        List<Category> categoryList = null;
        if (StringUtils.isBlank(allCatJson)) {
            categoryList = categoryService.queryCategoryList();
            redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
        } else {
            categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
        }

        return GraceJSONResult.ok(categoryList);
    }

admin端维护数据缓存 【文章领域】

文章分类 | 运营管理平台 (imoocnews.com)
在管理员修改文章类型后 【课网 → 课课】
媒体号作家中心 | 发文章 (imoocnews.com)
回到用户发文章的文章领域类型也会一起修改
慕课新闻 | 风间影月 (imoocnews.com)
同时首页上方的栏目框也会修改

service-admin  com/imooc/admin/service/impl/CategoryServiceImpl.java
@Service
public class CategoryServiceImpl extends BaseService implements CategoryService {
    @Autowired
    public CategoryMapper categoryMapper;

    @Transactional
    @Override
    public void createCategory(Category category) {
// 分类不会很多,所以id不需要自增,这个表的数据也不会多到几万甚至分表,数据都会集中在一起
        int result = categoryMapper.insert(category);
        if (result != 1){
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
            /**
             * 不建议如下做法:
             * 1. 查询redis中的categoryList
             * 2. 转化categoryList为list类型
             * 3. 在categoryList中add一个当前的category
             * 4. 再次转换categoryList为json,并存入redis中
             */
            // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
            redis.del(REDIS_ALL_CATEGORY);
        }
    }

    @Transactional
    @Override
    public void modifyCategory(Category category) {
        int result = categoryMapper.updateByPrimaryKey(category);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
        // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
        redis.del(REDIS_ALL_CATEGORY);
    }
......

发布文章入库Controller及验证【发头条】

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
package com.imooc.api.controller.article;

@Api(value = "文章业务的controller", tags = {"文章业务的controller"})
@RequestMapping("article")
public interface ArticleControllerApi {
    @PostMapping("createArticle")
    @ApiOperation(value = "用户发文", notes = "用户发文", httpMethod = "POST")
    public GraceJSONResult createArticle(@RequestBody @Valid NewArticleBO newArticleBO, BindingResult result);
}
service-article  com/imooc/article/controller/ArticleController.java
package com.imooc.article.controller;

@RestController
public class ArticleController extends BaseController implements ArticleControllerApi {
    final static Logger logger = LoggerFactory.getLogger(ArticleController.class);

    @Override
    public GraceJSONResult createArticle(NewArticleBO newArticleBO, BindingResult result) {
            if (result.hasErrors()){
                // 判断BindingResult是否保存错误的验证信息,如果有,则直接return
                Map<String, String> errorMap = getErrors(result);
                return GraceJSONResult.errorMap(errorMap);
            }
            // 判断文章封面类型,单图必填,纯文字则设置为空
            if (newArticleBO.getArticleType() == ArticleCoverType.ONE_IMAGE.type){
                if (StringUtils.isBlank(newArticleBO.getArticleCover())){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_CATEGORY_NOT_EXIST_ERROR);
                }
            } else if (newArticleBO.getArticleType() == ArticleCoverType.WORDS.type) {
                newArticleBO.setArticleCover("");
            }

        // 判断分类id是否存在
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);
        Category temp = null;
        if (StringUtils.isBlank(allCatJson)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        } else {
            List<Category> catList =
                    JsonUtils.jsonToList(allCatJson, Category.class);
            for (Category c : catList) {
                if(c.getId() == newArticleBO.getCategoryId()) {
                    temp = c;
                    break;
                }
            }
            if (temp == null) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_CATEGORY_NOT_EXIST_ERROR);
            }
        }
        return GraceJSONResult.ok();
    }
}
http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html
dev-model  com/imooc/pojo/bo/NewArticleBO.java
package com.imooc.pojo.bo;

import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
 * 用户发文的BO
 */
public class NewArticleBO {

    @NotBlank(message = "文章标题不能为空")
    @Length(max = 30, message = "文章标题长度不能超过30")
    private String title;

    @NotBlank(message = "文章内容不能为空")
    @Length(max = 9999, message = "文章内容长度不能超过10000")
    private String content;

    @NotNull(message = "请选择文章领域")
    private Integer categoryId;

    @NotNull(message = "请选择正确的文章封面类型")
    @Min(value = 1, message = "请选择正确的文章封面类型")
    @Max(value = 2, message = "请选择正确的文章封面类型")
    private Integer articleType;
    private String articleCover;

    @NotNull(message = "文章发布类型不正确")
    @Min(value = 0, message = "文章发布类型不正确")
    @Max(value = 1, message = "文章发布类型不正确")
    private Integer isAppoint;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") // 前端日期字符串传到后端后,转换为Date类型
    private Date publishTime;

    @NotBlank(message = "用户未登录")
    private String publishUserId;
}Getter + Setter

发布文章入库Service及联调【也可以定时发布】

http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html
发布完成后去数据库article中就会存在数据了

generator-datebase  generatorConfig-article.xml [逆向生成]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.article" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.article.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="comments"></table>

    </context>
</generatorConfiguration>
generator-datebase  com/imooc/mybatis/utils/ArticleGenerator.java
//[运行时候就会自动生成对应文件 目录是上面的generatorConfig-article.xml]
package com.imooc.mybatis.utils;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;


public class ArticleGenerator {

    public void generator() throws Exception {

        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        //指定 逆向工程配置文件
        File configFile = new File("mybatis-generator-database"
                                            + File.separator
                                            + "generatorConfig-article.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        myBatisGenerator.generate(null);

    } 
    
    public static void main(String[] args) throws Exception {
        try {
            ArticleGenerator generatorSqlmap = new ArticleGenerator();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
service-article  mapper/ArticleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.ArticleMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Article" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="title" property="title" jdbcType="VARCHAR" />
    <result column="category_id" property="categoryId" jdbcType="INTEGER" />
    <result column="article_type" property="articleType" jdbcType="INTEGER" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="is_appoint" property="isAppoint" jdbcType="INTEGER" />
    <result column="article_status" property="articleStatus" jdbcType="INTEGER" />
    <result column="publish_user_id" property="publishUserId" jdbcType="VARCHAR" />
    <result column="publish_time" property="publishTime" jdbcType="TIMESTAMP" />
    <result column="read_counts" property="readCounts" jdbcType="INTEGER" />
    <result column="comment_counts" property="commentCounts" jdbcType="INTEGER" />
    <result column="mongo_file_id" property="mongoFileId" jdbcType="VARCHAR" />
    <result column="is_delete" property="isDelete" jdbcType="INTEGER" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
    <result column="update_time" property="updateTime" jdbcType="TIMESTAMP" />
    <result column="content" property="content" jdbcType="LONGVARCHAR" />
  </resultMap>
</mapper>
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;

import java.util.List;

public interface ArticleService {
    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
package com.imooc.article.service.impl;

import com.imooc.api.service.BaseService;
import com.imooc.article.mapper.ArticleMapper;
import com.imooc.article.service.ArticleService;
import com.imooc.enums.ArticleAppointType;
import com.imooc.enums.ArticleReviewStatus;
import com.imooc.enums.YesOrNo;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Article;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

import static com.imooc.api.BaseController.REDIS_ALL_CATEGORY;


@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private Sid sid;

    @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }
    }
}
dev-model  com/imooc/pojo/Article.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;
import java.util.Date;

public class Article {
    @Id
    private String id;

    /**
     * 文章标题
     */
    private String title;

    /**
     * 文章所属分类id
     */
    @Column(name = "category_id")
    private Integer categoryId;

    /**
     * 文章类型,1:图文(1张封面),2:纯文字
     */
    @Column(name = "article_type")
    private Integer articleType;

    /**
     * 文章封面图,article_type=1 的时候展示
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 是否是预约定时发布的文章,1:预约(定时)发布,0:即时发布    在预约时间到点的时候,把1改为0,则发布
     */
    @Column(name = "is_appoint")
    private Integer isAppoint;

    /**
     * 文章状态,1:审核中(用户已提交),2:机审结束,等待人工审核,3:审核通过(已发布),4:审核未通过;5:文章撤回(已发布的情况下才能撤回和删除)
     */
    @Column(name = "article_status")
    private Integer articleStatus;

    /**
     * 发布者用户id
     */
    @Column(name = "publish_user_id")
    private String publishUserId;

    /**
     * 文章发布时间(也是预约发布的时间)
     */
    @Column(name = "publish_time")
    private Date publishTime;

    /**
     * 用户累计点击阅读数(喜欢数)(点赞) - 放redis
     */
    @Column(name = "read_counts")
    private Integer readCounts;

    /**
     * 文章评论总数。评论防刷,距离上次评论需要间隔时间控制几秒
     */
    @Column(name = "comment_counts")
    private Integer commentCounts;

    @Column(name = "mongo_file_id")
    private String mongoFileId;

    /**
     * 逻辑删除状态,非物理删除,1:删除,0:未删除
     */
    @Column(name = "is_delete")
    private Integer isDelete;

    /**
     * 文章的创建时间
     */
    @Column(name = "create_time")
    private Date createTime;

    /**
     * 文章的修改时间
     */
    @Column(name = "update_time")
    private Date updateTime;

    /**
     * 文章内容,长度不超过9999,需要在前后端判断
     */
    private String content;

构建定时任务 定时发布文章【定时任务】

在线Cron表达式生成器 (qqe2.com)

service-article  com/imooc/article/task/TaskPublishArticles.java
package com.imooc.article.task;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.time.LocalDateTime;

@Configuration  // 1.标记配置类,使得springboot容器扫描到
@EnableScheduling // 2.开启定时任务
public class TaskPublishArticles {
    @Scheduled(cron = "0/3 * * * * ? ")
    private void publishArticles(){
        System.out.println("执行定时任务:" + LocalDateTime.now());
    }
}

=================================================================
执行定时任务:2024-07-22T14:34:54.009
执行定时任务:2024-07-22T14:34:57.013
执行定时任务:2024-07-22T14:35:00.012
执行定时任务:2024-07-22T14:35:03.002
执行定时任务:2024-07-22T14:35:06.001
执行定时任务:2024-07-22T14:35:09.006
service-article  com/imooc/article/task/TaskPublishArticles.java
package com.imooc.article.task;

import com.imooc.article.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.time.LocalDateTime;

@Configuration  // 1.标记配置类,使得springboot容器扫描到
@EnableScheduling // 2.开启定时任务
public class TaskPublishArticles {
    @Autowired
    private ArticleService articleService;
    // 添加定时任务,注明定时任务的表达式
    // 【若文章数量庞大 需要RabbitMQ去做优化 后面会讲!】
    @Scheduled(cron = "0/3 * * * * ? ")
    private void publishArticles(){
        System.out.println("执行定时任务:" + LocalDateTime.now());
        // 4. 调用文章service,把当前时间应该发布的定时文章,状态改为即时
        articleService.updateAppointToPublish();
    }
}
service-article  com/imooc/article/mapper/ArticleMapperCustom.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Article;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleMapperCustom extends MyMapper<Article> {
    public void updateAppointToPublish();
}
service-article resources/mapper/ArticleMapperCustom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.ArticleMapperCustom" >
    <update id="updateAppointToPublish">
        update
            article
        set
            is_appoint = 0
        where
            publish_time &lt;= NOW()
        and
            is_appoint = 1
    </update>
</mapper>
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;

import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

}
service-article   com/imooc/article/service/impl/ArticleServiceImpl.java

@Transactional //添加事务[更新操作]
    @Override
    public void updateAppointToPublish() {
        articleMapperCustom.updateAppointToPublish();
    }

文章列表展示 【内容管理】

[mybatis中关于example类详解mybatis的Example

Criteria]的使用 - 万事俱备就差个程序员 - 博客园 (cnblogs.com)

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
@Api(value = "文章业务的controller", tags = {"文章业务的controller"})
@RequestMapping("article")
public interface ArticleControllerApi {

    @PostMapping("createArticle")
    @ApiOperation(value = "用户发文", notes = "用户发文", httpMethod = "POST")
    public GraceJSONResult createArticle(@RequestBody @Valid NewArticleBO newArticleBO, BindingResult result);

    @PostMapping("queryMyList") //对应着前端contentMng.html 340行
    @ApiOperation(value = "查询用户的所有文章列表", notes = "查询用户的所有文章列表", httpMethod = "POST")
    public GraceJSONResult queryMyList(@RequestParam String userId,
                                       @RequestParam String keyword,
                                       @RequestParam Integer status,
                                       @RequestParam Date startDate,
                                       @RequestParam Date endDate,
                                       @RequestParam Integer page,
                                       @RequestParam Integer pageSize);
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult queryMyList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_QUERY_PARAMS_ERROR);
        }
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }
        // 查询我的列表,调用service
        PagedGridResult grid =  articleService.queryMyArticleList(userId, keyword, status, startDate, endDate, page, pageSize);
        return GraceJSONResult.ok(grid);
    }
=========================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

    /**
     * 用户中心-查询我的文章列表
     */
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private ArticleMapperCustom articleMapperCustom;
    @Autowired
    private Sid sid;
     //匹配到前端的一种显示方法
    @Override
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        Example example = new Example(Article.class);
        example.orderBy("createTime").desc();
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("publishUserId", userId);
        if (StringUtils.isNotBlank(keyword)){
            //模糊查询
            criteria.andLike("title", "%"+keyword+"%");
        }
        if (ArticleReviewStatus.isArticleStatusValid(status)){
            // 有效就匹配 无效就查询所有
            criteria.andEqualTo("articleStatus", status);
        }
        // 12是在前端显示审核中
        if (status != null && status == 12){
            criteria.andEqualTo("articleStatus", ArticleReviewStatus.REVIEWING.type)
                    .orEqualTo("articleStatus", ArticleReviewStatus.WAITING_MANUAL.type);
        }
        // 逻辑删除
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        if (startDate != null){ //大于等于
            criteria.andGreaterThanOrEqualTo("publishTime", startDate);
        }
        if (startDate != null){ //小于等于
            criteria.andLessThanOrEqualTo("publishTime",endDate);
        }
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(example);
        return setterPagedGrid(list,page);
    }
/*
ArticleMapper 可以实现 selectByExample 是因为它继承了 MyMapper 接口,而 MyMapper 提供了一些通用的 CRUD 操作,这些操作包括 selectByExample。

selectByExample 是 MyBatis 提供的一种动态查询方法。它允许你根据条件动态地生成 SQL 查询,而不需要手动编写复杂的 SQL 语句。这在实际开发中非常方便,因为你可以通过构建 Example 对象来动态设置查询条件。

ArticleMapper 继承了 MyMapper<Article>,这意味着它自动获得了 MyMapper 中定义的所有方法,包括 selectByExample。MyMapper 是一个通用的 Mapper 接口,封装了常用的数据库操作方法。

Example 和 Criteria
Example: 用于构建查询条件的对象。在这里,我们创建了一个 Example 对象,用于设置查询的表(Article.class)和排序规则(按 createTime 降序)。

Criteria: 用于添加具体的查询条件。在 Example 对象中创建 Criteria 对象,并使用它来添加各种条件(例如 publishUserId、title、articleStatus、isDelete 等)。

selectByExample
selectByExample 方法使用 Example 对象中的条件动态生成 SQL 查询,并从数据库中获取符合条件的记录。在这个例子中,我们使用了 articleMapper.selectByExample(example) 来根据构建的 Example 对象进行查询。

Example 详细用法
Example 和 Criteria 的使用使得我们可以非常灵活地构建查询条件,而不需要直接拼接 SQL 语句。这不仅提高了代码的可读性,还减少了 SQL 注入的风险。
*/
service-article  com/imooc/article/mapper/ArticleMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Article;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleMapper extends MyMapper<Article> {
}

阿里AI文本检测【内容审核】[机器审核]

dev-common pom.xml 
       <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-green</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>
dev-common  com/imooc/utils/extend/AliTextReviewUtils.java
package com.imooc.utils.extend;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.green.model.v20180509.TextScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.util.*;

@Component
public class AliTextReviewUtils {

    @Autowired
    private AliyunResource aliyunResource;

    public String reviewTextContent(String content) {
        IClientProfile profile = DefaultProfile.getProfile("cn-shanghai",
                aliyunResource.getAccessKeyID(),
                aliyunResource.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);
        TextScanRequest textScanRequest = new TextScanRequest();
        textScanRequest.setAcceptFormat(FormatType.JSON); // 指定api返回格式
        textScanRequest.setHttpContentType(FormatType.JSON);
        textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST); // 指定请求方法
        textScanRequest.setEncoding("UTF-8");
        textScanRequest.setRegionId("cn-shanghai");
        List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
        Map<String, Object> task1 = new LinkedHashMap<String, Object>();
        task1.put("dataId", UUID.randomUUID().toString());
        /**
         * 待检测的文本,长度不超过10000个字符
         */
//        抵制毒品交易
//          尼玛
        task1.put("content", content);
        tasks.add(task1);
        JSONObject data = new JSONObject();

        /**
         * 检测场景,文本垃圾检测传递:antispam
         **/
        data.put("scenes", Arrays.asList("antispam"));
        data.put("tasks", tasks);
        System.out.println(JSON.toJSONString(data, true));

        try {
            textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
            // 请务必设置超时时间
            textScanRequest.setConnectTimeout(3000);
            textScanRequest.setReadTimeout(6000);

            HttpResponse httpResponse = client.doAction(textScanRequest);
            if(httpResponse.isSuccess()){
                JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
                System.out.println(JSON.toJSONString(scrResponse, true));
                if (200 == scrResponse.getInteger("code")) {
                    JSONArray taskResults = scrResponse.getJSONArray("data");
                    for (Object taskResult : taskResults) {
                        if(200 == ((JSONObject)taskResult).getInteger("code")){
                            JSONArray sceneResults = ((JSONObject)taskResult).getJSONArray("results");
                            JSONObject sceneResult = (JSONObject)sceneResults.get(0);
        //                            for (Object sceneResult : sceneResults) {
                                String scene = sceneResult.getString("scene");
                                String suggestion = sceneResult.getString("suggestion");
                                //根据scene和suggetion做相关处理
                                //suggestion == pass 未命中垃圾  suggestion == block 命中了垃圾,可以通过label字段查看命中的垃圾分类
                                System.out.println("args = [" + scene + "]");
                                System.out.println("args = [" + suggestion + "]");

        //                            suggestion=pass:文本正常,文章状态改为发布通过
        //                            review:需要人工审核,需要在后台管理系统中进行人工审核(很多自媒体平台都会采用机审+人工审的方式)
        //                            block:文本违规,可以直接删除或者做限制处理,审核不通过
        //                            }
                                return suggestion;
                        }else{
                            System.out.println("task process fail:" + ((JSONObject)taskResult).getInteger("code"));
                            return null;
                        }
                    }
                } else {
                    System.out.println("detect not success. code:" + scrResponse.getInteger("code"));
                    return null;
                }
            }else{
                System.out.println("response not success. status:" + httpResponse.getStatus());
                return null;
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
        return null;
    }
}

实现阿里AI自动审核文章【内容审核】

【沿用上面的AliTextReviewUtils】
@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private ArticleMapperCustom articleMapperCustom;
    @Autowired
    private AliTextReviewUtils aliTextReviewUtils;
    @Autowired
    private Sid sid;

    @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }

        /**
         * FIXME: 我们只检测正常的词汇,非正常词汇大家课后去检测
         */
        // 通过阿里智能AI实现对文章文本的自动检测(自动审核)
//        String reviewTextResult = aliTextReviewUtils.reviewTextContent(newArticleBO.getContent());
        String reviewTextResult = ArticleReviewLevel.REVIEW.type;

        if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.PASS.type)) {
            // 修改当前的文章,状态标记为审核通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.SUCCESS.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.REVIEW.type)) {
            // 修改当前的文章,状态标记为需要人工审核
            this.updateArticleStatus(articleId, ArticleReviewStatus.WAITING_MANUAL.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.BLOCK.type)) {
            // 修改当前的文章,状态标记为审核未通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.FAILED.type);
        }
    }
......
......
    @Transactional
    @Override
    public void updateArticleStatus(String articleId, Integer pendingStatus) {
        Example example = new Example(Article.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("id",articleId);

        Article pendingArticle = new Article();
        pendingArticle.setArticleStatus(pendingStatus);
        int res = articleMapper.updateByExampleSelective(pendingArticle, example);
        if (res != 1){
            GraceException.display(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
    }
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

    /**
     * 用户中心-查询我的文章列表
     */
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

    /**
     * 更改文章的状态
     * @param articleId
     * @param pendingStatus
     */
    public void updateArticleStatus(String articleId, Integer pendingStatus);
}

admin文章管理列表【内容审核】【作业】

管理员查询用户的所有文章列表
service-api  com/imooc/api/controller/article/ArticleControllerApi.java
@PostMapping("queryAllList")
    @ApiOperation(value = "管理员查询用户的所有文章列表", notes = "管理员查询用户的所有文章列表", httpMethod = "POST")
    public GraceJSONResult queryAllList(@RequestParam Integer status,
                                        @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                        @RequestParam Integer page,
                                        @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                        @RequestParam Integer pageSize);
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult queryAllList(Integer status, Integer page, Integer pageSize) {
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }
        PagedGridResult gridResult = articleService.queryAllArticleListAdmin(status,page,pageSize);

        return GraceJSONResult.ok(gridResult);
    }
service-article  com/imooc/article/service/ArticleService.java
    /**
     * 管理员查询文章列表
     */
    public PagedGridResult queryAllArticleListAdmin(Integer status, Integer page, Integer pageSize);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Override
    public PagedGridResult queryAllArticleListAdmin(Integer status, Integer page, Integer pageSize) {
        Example articleExample = new Example(Article.class);
        articleExample.orderBy("createTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();
        //这里是检测文章状态 与前端做匹配
        if (ArticleReviewStatus.isArticleStatusValid(status)) {
            criteria.andEqualTo("articleStatus", status);
        }

        // 审核中是机审和人审核的两个状态,所以需要单独判断
        if (status != null && status == 12) {
            criteria.andEqualTo("articleStatus", ArticleReviewStatus.REVIEWING.type)
                    .orEqualTo("articleStatus", ArticleReviewStatus.WAITING_MANUAL.type);
        }
        //isDelete必须是0
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, page);
    }

人工审核 【内容审核】

内容审核 | 运营管理平台 (imoocnews.com) 【[待审核]手动审核通过】

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
    @PostMapping("doReview")
    @ApiOperation(value = "管理员对文章进行审核通过或者失败", notes = "管理员对文章进行审核通过或者失败", httpMethod = "POST")
    public GraceJSONResult doReview(@RequestParam String articleId,
                                    @RequestParam Integer passOrNot);
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);
        return GraceJSONResult.ok();
    }

撤回_删除文章作业 【内容管理】

媒体号作家中心 | 内容管理 (imoocnews.com)
[这个是用户撤回和删除噢 而不是管理员的撤回与删除]
用户:媒体号作家中心 | 内容管理 (imoocnews.com)
管理员:内容审核 | 运营管理平台 (imoocnews.com)

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
    @PostMapping("/delete")
    @ApiOperation(value = "用户删除文章", notes = "用户删除文章", httpMethod = "POST")
    public GraceJSONResult delete(@RequestParam String userId,
                                  @RequestParam String articleId);

    @PostMapping("/withdraw")
    @ApiOperation(value = "用户撤回文章", notes = "用户撤回文章", httpMethod = "POST")
    public GraceJSONResult withdraw(@RequestParam String userId,
                                    @RequestParam String articleId);
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult delete(String userId, String articleId) {
        articleService.deleteArticle(userId,articleId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult withdraw(String userId, String articleId) {
        articleService.withdrawArticle(userId, articleId);
        return GraceJSONResult.ok();
    }
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

   /**
     * 删除文章
     */
    public void deleteArticle(String userId, String articleId);

    /**
     * 撤回文章
     */
    public void withdrawArticle(String userId, String articleId);
}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
 @Transactional
    @Override
    public void deleteArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setIsDelete(YesOrNo.YES.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_DELETE_ERROR);
        }
    }

    @Transactional
    @Override
    public void withdrawArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setArticleStatus(ArticleReviewStatus.WITHDRAW.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_WITHDRAW_ERROR);
        }
//        deleteHTML(articleId);
    }

    private Example makeExampleCriteria(String userId, String articleId) {
        Example articleExample = new Example(Article.class);
        Example.Criteria criteria = articleExample.createCriteria();
        criteria.andEqualTo("publishUserId", userId);
        criteria.andEqualTo("id", articleId);
        return articleExample;
    }

首页_作者页面介绍【章节描述】

  • 开发首页与作家个人展示页
  • 文章列表、友情链接查询
  • 粉丝关注与取关
  • 我的粉丝与粉丝画像

根据MongoDB字段查询友情链接

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
    @ApiOperation(value = "门户端查询友情链接列表", notes = "门户端查询友情链接列表", httpMethod = "GET")
    @GetMapping("portal/list")
    public GraceJSONResult queryPortalAllFriendLinkList();
service-admin  com/imooc/admin/controller/FriendLinkController.java
    @Override
    public GraceJSONResult queryPortalAllFriendLinkList() {
        List<FriendLinkMO> list = friendLinkService.queryPortalAllFriendLinkList();
        return GraceJSONResult.ok(list);
    }
service-admin  com/imooc/admin/service/FriendLinkService.java
    /**
     * 首页查询友情链接
     */
    public List<FriendLinkMO> queryPortalAllFriendLinkList();
service-admin com/imooc/admin/service/impl/FriendLinkServiceImpl.java 
@Override
    public List<FriendLinkMO> queryPortalAllFriendLinkList() {
        return friendLinkRepository.getAllByIsDelete(YesOrNo.NO.type);
    }
service-admin  com/imooc/admin/repository/FriendLinkRepository.java
@Repository
public interface FriendLinkRepository extends MongoRepository<FriendLinkMO, String> { //持久层
    // 内置提供了很多方法 find.. delete...
    public List<FriendLinkMO> getAllByIsDelete(Integer isDelete); //后面可以加ANDID
}

搜索并展示文章列表【首页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {
    @ApiOperation(value = "首页查询文章列表", notes = "首页查询文章列表", httpMethod = "GET")
    @GetMapping("list")
    public GraceJSONResult list(@RequestParam String keyword,
                                @RequestParam Integer category,
                                @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                @RequestParam Integer page,
                                @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                @RequestParam Integer pageSize);
}
service-article  com/imooc/article/controller/ArticlePortalController.java
@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
        return GraceJSONResult.ok(gridResult);
    }
}
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
@Service
public class ArticlePortalServiceImpl extends BaseService implements ArticlePortalService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository


    @Override
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize) {

        Example articleExample = new Example(Article.class);
        articleExample.orderBy("publishTime").desc();//使用时间进行排序
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        if (StringUtils.isNotBlank(keyword)) {
            criteria.andLike("title", "%" + keyword + "%");
        }
        if (category != null) {
            criteria.andEqualTo("categoryId", category);
        }

        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        System.out.println(keyword);
        System.out.println(category);
        return setterPagedGrid(list, page);
    }
}
service-article  com/imooc/article/service/ArticlePortalService.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 }
index.html
<!-- 中间容器 -->
        <div class="container">
            <!-- 文章列表 -->
            <div id="articleList" class="article-list">
                <ul>
                    <li class="single-article-wrapper" v-for="(article, index) in articleList" :key="index">
                        <img :src="article.articleCover" class="article-cover" v-show="article.articleType == 1">

                        <div class="single-article">
                            <div class="article-title">
                                <!-- TODO: 后期改为静态页面跳转 -->
                                <a :href="'detail.html?articleId='+article.id" target="_blank" class="link-article-title">{{article.title}}</a>
                            </div>
                            <div class="publisher">
                                <div class="category-tag" :style="{color: getCatTagColor(article.categoryId), borderColor: getCatTagColor(article.categoryId) }">{{getCatName(article.categoryId)}}</div>
                                <!-- TODO: 这里需要显示用户的昵称以及用户头像 -->
                                    <img src="img/face1.png" class="publisher-face" v-if="article.publisherVO == null || article.publisherVO == undefined">
                                    <div class="publisher-name" v-if="article.publisherVO == null || article.publisherVO == undefined">&nbsp;&nbsp;{{article.publishUserId}}&nbsp;⋅</div>
                                
                                
                                    <img :src="article.publisherVO.face" class="publisher-face" v-if="article.publisherVO != null && article.publisherVO != undefined">
                                <!--
                                    <a :href="'writer.html?writerId='+article.publisherVO.id" target="_blank">
                                    <div class="publisher-name" v-if="article.publisherVO != null && article.publisherVO != undefined">&nbsp;&nbsp;{{article.publisherVO.nickname}}&nbsp;⋅</div>
                                </a>
                                    -->

                                <div class="article-name">&nbsp;{{article.readCounts}}阅读&nbsp;⋅</div>
                                
                                <!-- <div class="publish-time">&nbsp;{{formatData(article.publishTime)}}</div> -->
                                <div class="publish-time">&nbsp;{{getDateBeforeNow(article.publishTime)}}</div>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>

文章列表展示发布者需求【首页】

service-article  com/imooc/article/controller/ArticlePortalController.java
[其他不变加上点代码]
package com.imooc.article.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.article.ArticleControllerApi;
import com.imooc.api.controller.article.ArticlePortalControllerApi;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.ArticleService;
import com.imooc.enums.ArticleCoverType;
import com.imooc.enums.ArticleReviewStatus;
import com.imooc.enums.YesOrNo;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Article;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表

        // 3. 拼接两个list,重组文章列表
   //END
        return GraceJSONResult.ok(gridResult);
    }
}

发起restTemplate请求查询用户服务获得发布者列表【首页】二级用户

service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START 用户量大就双表关联查询      单表双查询 → 【首页不会显示发布者的用户id 和 头像】
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idset);
        ResponseEntity<GraceJSONResult> responseEntity =
        restTemplate.getForEntity(userServerUrlExecute,GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200){
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        for (AppUserVO u : publisherList){
            System.out.println(u.toString());
        }
        // 3. 拼接两个list,重组文章列表
   //END
        return GraceJSONResult.ok(gridResult);
    }
===================成功输出二级用户基本信息===============================
AppUserVO{id='240629F21AK1BHX4', nickname='15027597319', face='https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/240629F21AK1BHX4/240712FM0G1WMZHH.png', activeStatus=1}
AppUserVO{id='200628AFYM7AGWPH', nickname='我是慕课网', face='https://imooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/200628AFYM7AGWPH/2007088XH2WT7GXP.png', activeStatus=1}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;
}Getter + Setter + ToString
service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
@Override
    public GraceJSONResult queryByIds(String userIds) {
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }

    private AppUserVO getBasicUserInfo(String userId){
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        return userVO;
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          BindingResult result);

    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByids")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}

重组文章列表并且渲染【首页】

index.html
<!-- 中间容器 -->
        <div class="container">
            <!-- 文章列表 -->
            <div id="articleList" class="article-list">
                <ul>
                    <li class="single-article-wrapper" v-for="(article, index) in articleList" :key="index">
                        <img :src="article.articleCover" class="article-cover" v-show="article.articleType == 1">

                        <div class="single-article">
                            <div class="article-title">
                                <!-- TODO: 后期改为静态页面跳转 -->
                                <a :href="'detail.html?articleId='+article.id" target="_blank" class="link-article-title">{{article.title}}</a>
                            </div>
                            <div class="publisher">
                                <div class="category-tag" :style="{color: getCatTagColor(article.categoryId), borderColor: getCatTagColor(article.categoryId) }">{{getCatName(article.categoryId)}}</div>
          <!-- ★★★★★ TODO: 这里需要显示用户的昵称以及用户头像 ★★★★★ -->
                                    <img src="img/face1.png" class="publisher-face" v-if="article.publisherVO == null || article.publisherVO == undefined">
                                    <div class="publisher-name" v-if="article.publisherVO == null || article.publisherVO == undefined">&nbsp;&nbsp;{{article.publishUserId}}&nbsp;⋅</div>
                                    <img :src="article.publisherVO.face" class="publisher-face" v-if="article.publisherVO != null && article.publisherVO != undefined">
                                
                                    <a :href="'writer.html?writerId='+article.publisherVO.id" target="_blank">
                                    <div class="publisher-name" v-if="article.publisherVO != null && article.publisherVO != undefined">&nbsp;&nbsp;{{article.publisherVO.nickname}}&nbsp;⋅</div>
                                </a>            
                                <div class="article-name">&nbsp;{{article.readCounts}}阅读&nbsp;⋅</div>
                                <!-- <div class="publish-time">&nbsp;{{formatData(article.publishTime)}}</div> -->
                                <div class="publish-time">&nbsp;{{getDateBeforeNow(article.publishTime)}}</div>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>
dev-model  com/imooc/pojo/vo/IndexArticleVO.java
public class IndexArticleVO {

    private String id;
    private String title;
    private Integer categoryId;
    private Integer articleType;
    private String articleCover;
    private Integer isAppoint;
    private Integer articleStatus;
    private String publishUserId;
    private Date publishTime;
    private Integer readCounts;
    private Integer commentCounts;
    private String mongoFileId;
    private Integer isDelete;
    private Date createTime;
    private Date updateTime;
    private String content;
}Getter+Setter
service-article  com/imooc/article/controller/ArticlePortalController.java
package com.imooc.article.controller;
......
import java.util.*;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START 用户量大就双表关联查询      单表双查询 → 【首页不会显示发布者的用户id 和 头像】
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idset);
        ResponseEntity<GraceJSONResult> responseEntity =
        restTemplate.getForEntity(userServerUrlExecute,GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200){
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
//        for (AppUserVO u : publisherList){
//            System.out.println(u.toString());
//        }
        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (Article a : list){
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);
            indexArticleList.add(indexArticleVO);
        }
        gridResult.setRows(indexArticleList);
   //END
        return GraceJSONResult.ok(gridResult);
    }
    // 用于获得publish
    private AppUserVO getUserIfPublisher(String publisherId, List<AppUserVO> publisherList){
        for (AppUserVO user : publisherList){
            if (user.getId().equalsIgnoreCase(publisherId)){
                return user;
            }
        }
        return null;
    }
}

查询热闻【首页】阅读数从最新新闻进行排名

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {
 @GetMapping("hotList")
    @ApiOperation(value = "首页查询新闻列表", notes = "首页查询新闻列表", httpMethod = "GET")
    public GraceJSONResult hotList();
}
service-article  com/imooc/article/service/ArticlePortalService.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize);
    /**
     * 首页查询热闻列表
     */
    public List<Article> queryHotList();
}
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
@Override
    public List<Article> queryHotList() {
        Example articleExample = new Example(Article.class);
        Example.Criteria criteria = setDefualArticleExample(articleExample);

        PageHelper.startPage(1, 5);
        List<Article> list  = articleMapper.selectByExample(articleExample);
        return list;
    }

    private Example.Criteria setDefualArticleExample(Example articleExample) {
        articleExample.orderBy("publishTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        return criteria;
    }
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult hotList() {
        return GraceJSONResult.ok(articlePortalService.queryHotList());
    }

基本信息展示_历史文章列表【作者主页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
 package com.imooc.api.controller.article;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewArticleBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Date;

@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {

    @GetMapping("list")
    @ApiOperation(value = "首页查询文章列表", notes = "首页查询文章列表", httpMethod = "GET")
    public GraceJSONResult list(@RequestParam String keyword,
                                @RequestParam Integer category,
                                @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                @RequestParam Integer page,
                                @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                @RequestParam Integer pageSize);

    @GetMapping("hotList")
    @ApiOperation(value = "首页查询新闻列表", notes = "首页查询新闻列表", httpMethod = "GET")
    public GraceJSONResult hotList();

    /**
     * 查询作家发布的所有文章列表
     */
    @GetMapping("queryArticleListOfWriter")
    @ApiOperation(value = "查询作家发布的所有文章列表", notes = "查询作家发布的所有文章列表", httpMethod = "GET")
    public GraceJSONResult queryArticleListOfWriter(@RequestParam String writerId,
                                                    @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                                        @RequestParam Integer page,
                                                    @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                                        @RequestParam Integer pageSize);

    @GetMapping("queryGoodArticleListOfWriter")
    @ApiOperation(value = "作家页面查询近期佳文", notes = "作家页面查询近期佳文", httpMethod = "GET")
    public GraceJSONResult queryGoodArticleListOfWriter(@RequestParam String writerId);
}
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {

        System.out.println("writerId=" + writerId);

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = articlePortalService.queryArticleListOfWriter(writerId, page, pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult queryGoodArticleListOfWriter(String writerId) {
        PagedGridResult gridResult = articlePortalService.queryGoodArticleListOfWriter(writerId);
        return GraceJSONResult.ok(gridResult);
    }
}
/* 完全版ArticlePortalController
package com.imooc.article.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.article.ArticlePortalControllerApi;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.ArticleService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.vo.AppUserVO;
import com.imooc.pojo.vo.IndexArticleVO;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    private PagedGridResult rebuildArticleGrid(PagedGridResult gridResult) {
        // START

        List<Article> list = (List<Article>)gridResult.getRows();

        // 1. 构建发布者id列表
        Set<String> idSet = new HashSet<>();
        List<String> idList = new ArrayList<>();
        for (Article a : list) {
//            System.out.println(a.getPublishUserId());
            // 1.1 构建发布者的set
            idSet.add(a.getPublishUserId());
            // 1.2 构建文章id的list
            idList.add(REDIS_ARTICLE_READ_COUNTS + ":" + a.getId());
        }
        System.out.println(idSet.toString());
        // 发起redis的mget批量查询api,获得对应的值
        List<String> readCountsRedisList = redis.mget(idList);
        List<AppUserVO> publisherList = getPublisherList(idSet);

        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (int i = 0 ; i < list.size() ; i ++) {
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            Article a = list.get(i);
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher  = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);

            // 3.2 重新组装设置文章列表中的阅读量
            String redisCountsStr = readCountsRedisList.get(i);
            int readCounts = 0;
            if (StringUtils.isNotBlank(redisCountsStr)) {
                readCounts = Integer.valueOf(redisCountsStr);
            }
            indexArticleVO.setReadCounts(readCounts);

            indexArticleList.add(indexArticleVO);
        }


        gridResult.setRows(indexArticleList);
// END
        return gridResult;
    }

    private AppUserVO getUserIfPublisher(String publisherId,
                                         List<AppUserVO> publisherList) {
        for (AppUserVO user : publisherList) {
            if (user.getId().equalsIgnoreCase(publisherId)) {
                return user;
            }
        }
        return null;
    }

    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }

    @Override
    public GraceJSONResult hotList() {
        return GraceJSONResult.ok(articlePortalService.queryHotList());
    }

    @Override
    public GraceJSONResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {

        System.out.println("writerId=" + writerId);

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = articlePortalService.queryArticleListOfWriter(writerId, page, pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult queryGoodArticleListOfWriter(String writerId) {
        PagedGridResult gridResult = articlePortalService.queryGoodArticleListOfWriter(writerId);
        return GraceJSONResult.ok(gridResult);
    }
}

*/
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize);
    /**
     * 首页查询热闻列表
     */
    public List<Article> queryHotList();

    /**
     * 查询作家发布的所有文章列表
     */
    public PagedGridResult queryArticleListOfWriter(String writerId,
                                                    Integer page,
                                                    Integer pageSize);

    /**
     * 作家页面查询近期佳文
     */
    public PagedGridResult queryGoodArticleListOfWriter(String writerId);
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java

@Override
    public PagedGridResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {
        Example articleExample = new Example(Article.class);

        Example.Criteria criteria = setDefualArticleExample(articleExample);
        criteria.andEqualTo("publishUserId", writerId);

        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, page);
    }

    @Override
    public PagedGridResult queryGoodArticleListOfWriter(String writerId) {
        Example articleExample = new Example(Article.class);
        articleExample.orderBy("publishTime").desc();

        Example.Criteria criteria = setDefualArticleExample(articleExample);
        criteria.andEqualTo("publishUserId", writerId);

        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(1, 5);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, 1);
    }

    private Example.Criteria setDefualArticleExample(Example articleExample) {
        articleExample.orderBy("publishTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        return criteria;
    }

关注与取关_redis单线程计数统计 【粉丝关注】

阅读数可以用数据库COUNT* 但是压力会很大 若很多人一起刷新会音响很大
用redis 数量累加累减 单线程安全
减少数据库压力

【注意 redis我安装到了本地计算机里面 D:\Redis-x64-3.0.504】
打开redis-cli.exe
127.0.0.1:6379> keys *
1) "redis_all_category"
2) "redis_admin_token:1001"
3) "redis_user_info:1001"
4) "redis_user_info:200628AFYM7AGWPH"
5) "redis_user_token:240629F21AK1BHX4"
6) "redis_user_info:240629F21AK1BHX4"
7) "redis_user_token:200628AFYM7AGWPH"
127.0.0.1:6379> INCR 1001:fans #【增加】
(integer) 1
127.0.0.1:6379> INCR 1001:fans
(integer) 2
127.0.0.1:6379> INCR 1001:fans
(integer) 3
127.0.0.1:6379> get 1001:fans #【获取】
"3"
127.0.0.1:6379> DECR 1001:fans #【减少】
(integer) 2
127.0.0.1:6379> DECR 1001:fans
(integer) 1
127.0.0.1:6379> incr 1001:follows #【关注的粉丝】
(integer) 1
127.0.0.1:6379> get 1001:follows
"1"

查询用户关注状态【粉丝关注】

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
    @ApiOperation(value = "查询当前用户是否关注作家",notes = "查询当前用户是否关注作家",httpMethod = "POST")
    @PostMapping("/isMeFollowThisWriter")
    public GraceJSONResult isMeFollowThisWriter(@RequestParam String writerId, @RequestParam String fanId);
}
service-user  com/imooc/user/controller/MyFansController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.MyFansControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
    final static Logger logger = LoggerFactory.getLogger(MyFansController.class);

    @Autowired
    private MyFansService myFansService;

    @Override
    public GraceJSONResult isMeFollowThisWriter(String writerId, String fanId) {
        boolean res = myFansService.isMeFollowThisWriter(writerId,fanId);
        return GraceJSONResult.ok(res);
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.Fans;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.mapper.FansMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
    @Autowired
    public FansMapper fansMapper;


    @Override
    public boolean isMeFollowThisWriter(String writerId, String fanId) {
        Fans fan = new Fans();
        fan.setFanId(fanId);
        fan.setWriterId(writerId);
        int count = fansMapper.selectCount(fan); //前期先放在数据库里
        return count > 0 ? true : false;
    }
}
service-user  com/imooc/user/mapper/FansMapper.java
package com.imooc.user.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Fans;
import org.springframework.stereotype.Repository;

@Repository
public interface FansMapper extends MyMapper<Fans> {
}

用户关注_粉丝累加 && 粉丝累减

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
    @ApiOperation(value = "用户关注作家,成为粉丝",notes = "用户关注作家,成为粉丝",httpMethod = "POST")
    @PostMapping("/follow")
    public GraceJSONResult follow(@RequestParam String writerId, @RequestParam String fanId);
    
    @ApiOperation(value = "取消关注,作家损失粉丝",notes = "取消关注,作家损失粉丝",httpMethod = "POST")
    @PostMapping("/unfollow")
    public GraceJSONResult unfollow(@RequestParam String writerId, @RequestParam String fanId);
}
service-user  com/imooc/user/controller/MyFansController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.MyFansControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.user.service.MyFansService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
    final static Logger logger = LoggerFactory.getLogger(MyFansController.class);

    @Autowired
    private MyFansService myFansService;

    @Override
    public GraceJSONResult isMeFollowThisWriter(String writerId, String fanId) {
        boolean res = myFansService.isMeFollowThisWriter(writerId,fanId);
        return GraceJSONResult.ok(res);
    }

    @Override
    public GraceJSONResult follow(String writerId, String fanId) {
        myFansService.follow(writerId,fanId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult unfollow(String writerId, String fanId) {
        myFansService.unfollow(writerId, fanId);
        return GraceJSONResult.ok();
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

    /**
     * 关注成为粉丝
     */
    public void follow(String writerId, String fanId);
    
    /**
     * 粉丝取消关注
     */
    public void unfollow(String writerId, String fanId);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.Fans;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.mapper.FansMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
    @Autowired
    public FansMapper fansMapper;
    @Override
    public boolean isMeFollowThisWriter(String writerId, String fanId) {
        Fans fan = new Fans();
        fan.setFanId(fanId);
        fan.setWriterId(writerId);
        int count = fansMapper.selectCount(fan); //前期先放在数据库里
        return count > 0 ? true : false;
    }
    
    @Transactional
    @Override
    public void follow(String writerId, String fanId) {
        // 获得粉丝用户的信息
        AppUser fanInfo = userService.getUser(fanId);

        String fanPkId = sid.nextShort();

        Fans fans = new Fans();
        fans.setId(fanPkId);
        fans.setFanId(fanId);
        fans.setWriterId(writerId);

        fans.setFace(fanInfo.getFace());
        fans.setFanNickname(fanInfo.getNickname());
        fans.setSex(fanInfo.getSex());
        fans.setProvince(fanInfo.getProvince());

        fansMapper.insert(fans);

        // redis 作家粉丝数累加
        redis.increment(REDIS_WRITER_FANS_COUNTS + ":" + writerId, 1);  //增加key一次
        // redis 当前用户的(我的)关注数累加
        redis.increment(REDIS_MY_FOLLOW_COUNTS + ":" + fanId, 1);  //增加key一次
    }
    
    @Transactional
    @Override
    public void unfollow(String writerId, String fanId) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);
        fans.setFanId(fanId);

        fansMapper.delete(fans);

        // redis 作家粉丝数累减
        redis.decrement(REDIS_WRITER_FANS_COUNTS + ":" + writerId, 1);  //增加key一次
        // redis 当前用户的(我的)关注数累减
        redis.decrement(REDIS_MY_FOLLOW_COUNTS + ":" + fanId, 1);  //增加key一次
    }
}
service-api  com/imooc/api/service/BaseService.java
package com.imooc.api.service;

import com.github.pagehelper.PageInfo;
import com.imooc.utils.PagedGridResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public class BaseService {
    public static final String REDIS_ALL_CATEGORY = "redis_all_category";

    public static final String REDIS_WRITER_FANS_COUNTS = "redis_writer_fans_counts";
    public static final String REDIS_MY_FOLLOW_COUNTS = "redis_my_follow_counts";

    public static final String REDIS_ARTICLE_COMMENT_COUNTS = "redis_article_comment_counts";

    @Autowired
    public RedisOperator redis;
    public PagedGridResult setterPagedGrid(List<?> list, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(list);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(list);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getTotal());
        gridResult.setTotal(pageList.getPages());
        return gridResult;
    }
}
service-api  com/imooc/api/config/InterceptorConfig.java //增加粉丝接口的拦截
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace")
                .addPathPatterns("/fs/uploadSomeFiles")
                .addPathPatterns("/fans/follow")
                .addPathPatterns("/fans/unfollow");

        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete")
                .addPathPatterns("/categoryMng/saveOrUpdateCategory")
                .addPathPatterns("/categoryMng/getCatList");

        registry.addInterceptor(userActiveInterceptor())
                .addPathPatterns("/fs/uploadSomeFiles")
                .addPathPatterns("/fans/follow")
                .addPathPatterns("/fans/unfollow");
    }
}

粉丝数与关注数页面显示【粉丝关注】

service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
 @Override
    public GraceJSONResult getUserInfo(String userId) {
        //接口进行解耦!!
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        // 3. 查询redis中用户的关注数和粉丝数,放入userVO放入前端渲染
        userVO.setMyFansCounts(getCountsFromRedis(REDIS_WRITER_FANS_COUNTS + ":" + userId));
        userVO.setMyFollowCounts(getCountsFromRedis(REDIS_MY_FOLLOW_COUNTS + ":" + userId));
        return GraceJSONResult.ok(userVO);
    }
}
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
public Integer getCountsFromRedis(String key){
        String countsStr = redis.get(key);
        if (StringUtils.isBlank(countsStr)) {
            countsStr = "0";
        }
        return Integer.valueOf(countsStr);
    }
}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;

    private Integer myFollowCounts;
    private Integer myFansCounts;
}Getter + Setter

我的粉丝列表_后端分页查询【粉丝管理】

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
@ApiOperation(value = "查询我的所有粉丝列表", notes = "查询我的所有粉丝列表", httpMethod = "POST")
    @PostMapping("/queryAll")
    public GraceJSONResult queryAll(
            @RequestParam String writerId,
            @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
            @RequestParam Integer page,
            @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false)
            @RequestParam Integer pageSize);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
 @Override
    public GraceJSONResult queryAll(String writerId, Integer page, Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        return GraceJSONResult.ok(myFansService.queryMyFansList(writerId, page, pageSize));
    }
}
===================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/myFans.html
数据库中fans表
weiter_id
可以改成登录的cookie里面的 uid:240629F21AK1BHX4
就可以测试用户粉丝数量
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

    /**
     * 关注成为粉丝
     */
    public void follow(String writerId, String fanId);

    /**
     * 粉丝取消关注
     */
    public void unfollow(String writerId, String fanId);

    /**
     * 查询我的粉丝
     */
    public PagedGridResult queryMyFansList(String writerId, Integer page, Integer pageSize);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public PagedGridResult queryMyFansList(String writerId, Integer page, Integer pageSize) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);

        PageHelper.startPage(page,pageSize); //进行分页
        List<Fans> list = fansMapper.select(fans);
        return setterPagedGrid(list,page);
    }
}

男女比例柱状图_饼状图显示【数据可视化-粉丝画像】Echarts

Apache ECharts + 前端 [myFansCharts-static.html + myFansCharts.html]

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
@ApiOperation(value = "查询男女粉丝数量", notes = "查询男女粉丝数量", httpMethod = "POST")
    @PostMapping("/queryRatio")
    public GraceJSONResult queryRatio(@RequestParam String writerId);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
 @Override
    public GraceJSONResult queryRatio(String writerId) {
        int manCount = myFansService.queryFansCounts(writerId, Sex.man);
        int womanCount = myFansService.queryFansCounts(writerId, Sex.woman);

        FansCountsVO fansCountsVO = new FansCountsVO();
        fansCountsVO.setManCounts(manCount);
        fansCountsVO.setWomanCounts(womanCount);
        return GraceJSONResult.ok(fansCountsVO);
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
     /**
     * 查询粉丝数
     */
    public Integer queryFansCounts(String writerId, Sex sex);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public Integer queryFansCounts(String writerId, Sex sex) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);
        fans.setSex(sex.type);

        int count = fansMapper.selectCount(fans);
        return count;
    }
}

中国地图粉丝地域分布数量展示【数据可视化-粉丝画像】

男女比例柱状图_饼状图显示【数据可视化-粉丝画像】Echarts

Apache ECharts + 前端 [myFansCharts-static.html + myFansCharts.html]

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@ApiOperation(value = "根据地域查询粉丝数量", notes = "根据地域查询粉丝数量", httpMethod = "POST")
    @PostMapping("/queryRatioByRegion")
    public GraceJSONResult queryRatioByRegion(@RequestParam String writerId);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
  @Override
    public GraceJSONResult queryRatioByRegion(String writerId) {
        return GraceJSONResult.ok(myFansService.queryRegionRatioCounts(writerId));
    }
}
=====================================================================
将fans里的writer_id【自己的cookie里的uid 属于自己的属性 对应着右面的province省份】
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
     /**
     * 查询粉丝数
     */
    public List<RegionRatioVO> queryRegionRatioCounts(String writerId);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public List<RegionRatioVO> queryRegionRatioCounts(String writerId) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);

        List<RegionRatioVO> list = new ArrayList<>();
        for (String r : regions) {
            fans.setProvince(r);
            Integer count = fansMapper.selectCount(fans);

            RegionRatioVO regionRatioVO = new RegionRatioVO();
            regionRatioVO.setName(r);
            regionRatioVO.setValue(count);

            list.add(regionRatioVO);
        }
        return list;
    }

    public static final String[] regions = {"北京", "天津", "上海", "重庆",
            "河北", "山西", "辽宁", "吉林", "黑龙江", "江苏", "浙江", "安徽", "福建", "江西", "山东",
            "河南", "湖北", "湖南", "广东", "海南", "四川", "贵州", "云南", "陕西", "甘肃", "青海", "台湾",
            "内蒙古", "广西", "西藏", "宁夏", "新疆",
            "香港", "澳门"};
}
dev-model  com/imooc/pojo/vo/RegionRatioVO.java
package com.imooc.pojo.vo;

public class RegionRatioVO {

    private String name;
    private Integer value;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }
}

开发文章详情接口 【章节概述】detail.html

  • 文章详情页
  • 文章评论模块
  • 评论管理

文章详情页

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
 @GetMapping("detail")
    @ApiOperation(value = "文章详情查询", notes = "文章详情查询", httpMethod = "GET")
    public GraceJSONResult detail(@RequestParam String articleId);
service-article  com/imooc/article/controller/ArticlePortalController.java
 @Override
    public GraceJSONResult detail(String articleId) {
        ArticleDetailVO detailVO = articlePortalService.queryDetail(articleId);

        Set<String> idSet = new HashSet();
        idSet.add(detailVO.getPublishUserId());
        List<AppUserVO> publisherList = getPublisherList(idSet);

        if (!publisherList.isEmpty()) {
            detailVO.setPublishUserName(publisherList.get(0).getNickname());
        }

        detailVO.setReadCounts(
                getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId));

        return GraceJSONResult.ok(detailVO);
    }
==================================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240721DDAHBPWG0H
service-article  com/imooc/article/service/ArticlePortalService.java
    /**
     * 查询文章详情
     */
    public ArticleDetailVO queryDetail(String articleId);
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
    @Override
    public ArticleDetailVO queryDetail(String articleId) {
        Article article = new Article();
        article.setId(articleId);
        article.setIsAppoint(YesOrNo.NO.type);
        article.setIsDelete(YesOrNo.NO.type);
        article.setArticleStatus(ArticleReviewStatus.SUCCESS.type);

        Article result = articleMapper.selectOne(article);
        ArticleDetailVO detailVO = new ArticleDetailVO();
        BeanUtils.copyProperties(result, detailVO);
        return detailVO;
    }
dev-model  com/imooc/pojo/vo/ArticleDetailVO.java
package com.imooc.pojo.vo;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter

阅读文章_阅读量redis累加【详情页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@PostMapping("readArticle")
    @ApiOperation(value = "阅读文章,文章阅读量累加", notes = "阅读文章,文章阅读量累加", httpMethod = "POST")
    public GraceJSONResult readArticle(@RequestParam String articleId);
service-article  com/imooc/article/controller/ArticlePortalController.java
  @Override
    public GraceJSONResult detail(String articleId) {
        ArticleDetailVO detailVO = articlePortalService.queryDetail(articleId);

        Set<String> idSet = new HashSet();
        idSet.add(detailVO.getPublishUserId());
        List<AppUserVO> publisherList = getPublisherList(idSet);

        if (!publisherList.isEmpty()) {
            detailVO.setPublishUserName(publisherList.get(0).getNickname());
        }

        detailVO.setReadCounts( //去redis获取值 关联到前端阅读量增加 关联!!!
                getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId));

        return GraceJSONResult.ok(detailVO);
    }

    @Override
    public GraceJSONResult readArticle(String articleId) {
        redis.increment(REDIS_ARTICLE_READ_COUNTS + ":" + articleId, 1);
        return GraceJSONResult.ok();
    }
==================================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240721DDAHBPWG0H
service-article  com/imooc/article/service/ArticlePortalService.java
public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter

文章阅读数防刷策略【详情页】

限定id去做增加 readArticle中增加拦截器
在ArticlePortalControllerApi.java中的readArticle接口 增加 HttpServletRequest request

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@PostMapping("readArticle")
    @ApiOperation(value = "阅读文章,文章阅读量累加", notes = "阅读文章,文章阅读量累加", httpMethod = "POST")
    public GraceJSONResult readArticle(@RequestParam String articleId, HttpServletRequest request);
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult readArticle(String articleId, HttpServletRequest request) {
        String userIp = IPUtil.getRequestIp(request);
        // 设置针对当前用户ip的永久存在的key,存入redis,表示该ip的用户已经阅读过了 防刷策略
        redis.setnx(REDIS_ALREADY_READ + ":" + articleId + ":" + userIp, userIp);

        redis.increment(REDIS_ARTICLE_READ_COUNTS + ":" + articleId, 1);
        return GraceJSONResult.ok();
    }
service-api  com/imooc/api/interceptors/ArticleReadInterceptor.java //【增加拦截器】
package com.imooc.api.interceptors;

import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ArticleReadInterceptor extends BaseInterceptor implements HandlerInterceptor {

    @Autowired
    public RedisOperator redis;
    public static final String REDIS_ALREADY_READ = "redis_already_read";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String articleId = request.getParameter("articleId");

        String userIp = IPUtil.getRequestIp(request);
        boolean isExist = redis.keyIsExist(REDIS_ALREADY_READ + ":" +  articleId + ":" + userIp);

        if (isExist) {
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
...
    @Bean
    public ArticleReadInterceptor articleReadInterceptor(){
        return new ArticleReadInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
...
        registry.addInterceptor(articleReadInterceptor())
                .addPathPatterns("/portal/article/readArticle");
    }
}

Redis mget 批量查询组装阅读量并展示【文章列表】

Redis get单个读取 && Redis mget批量读取

service-article  com/imooc/article/controller/ArticlePortalController.java
 private PagedGridResult rebuildArticleGrid(PagedGridResult gridResult) {
        // START
        List<Article> list = (List<Article>)gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idSet = new HashSet<>();
        List<String> idList = new ArrayList<>();
        for (Article a : list) {
//            System.out.println(a.getPublishUserId());
            // 1.1 构建发布者的set
            idSet.add(a.getPublishUserId());
            // 1.2 构建文章id的list 包含所有key的值
            idList.add(REDIS_ARTICLE_READ_COUNTS + ":" + a.getId());
        }
        System.out.println(idSet.toString());
        // 发起redis的mget批量查询api,获得对应的值
        List<String> readCountsRedisList = redis.mget(idList);
        List<AppUserVO> publisherList = getPublisherList(idSet);

        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (int i = 0 ; i < list.size() ; i ++) {
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            Article a = list.get(i); //属性值拷贝
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher  = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);

            // 3.2 重新组装设置文章列表中的阅读量
            String redisCountsStr = readCountsRedisList.get(i);
            int readCounts = 0;
            if (StringUtils.isNotBlank(redisCountsStr)) {
                readCounts = Integer.valueOf(redisCountsStr);
            }
            indexArticleVO.setReadCounts(readCounts);

            indexArticleList.add(indexArticleVO);
        }
        gridResult.setRows(indexArticleList);
// END
        return gridResult;
    }

用户发表评论【文章评论】

mybatis-generator-database generatorConfig-article.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.article" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.article.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="comments"></table>

    </context>
</generatorConfiguration>
mybatis-generator-database  com/imooc/mybatis/utils/ArticleGenerator.java //【运行】
package com.imooc.mybatis.utils;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;


public class ArticleGenerator {

    public void generator() throws Exception {

        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        //指定 逆向工程配置文件
        File configFile = new File("mybatis-generator-database"
                                            + File.separator
                                            + "generatorConfig-article.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        myBatisGenerator.generate(null);

    } 
    
    public static void main(String[] args) throws Exception {
        try {
            ArticleGenerator generatorSqlmap = new ArticleGenerator();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
service-article  com/imooc/article/mapper/CommentsMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Comments;

public interface CommentsMapper extends MyMapper<Comments> {
}

================================================================

service-article  resources/mapper/CommentsMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.article.mapper.CommentsMapper">
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="comment_user_face" jdbcType="VARCHAR" property="commentUserFace" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="comment_user_face" jdbcType="VARCHAR" property="commentUserFace" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
</mapper>
service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {

    @PostMapping("createComment")
    @ApiOperation(value = "用户评论", notes = "用户评论", httpMethod = "POST") //@Valid是做验证的
    public GraceJSONResult createArticle(@RequestBody @Valid CommentReplyBO commentReplyBO, BindingResult result);
service-article  com/imooc/article/controller/CommentController.java
@RestController
public class CommentController extends BaseController implements CommentControllerApi {
    final static Logger logger = LoggerFactory.getLogger(CommentController.class);

    @Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();
...[未完待续]
service-api  com/imooc/api/BaseController.java
public List<AppUserVO> getBasicUserList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> userVOList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            userVOList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return userVOList;
    }
dev-model  com/imooc/pojo/bo/CommentReplyBO.java
/**
 * 文章留言的BO
 */
public class CommentReplyBO {

    @NotBlank(message = "留言信息不完整")
    private String articleId;

    @NotBlank(message = "留言信息不完整")
    private String fatherId;

    @NotBlank(message = "当前用户信息不正确,请尝试重新登录")
    private String commentUserId;

    @NotBlank(message = "留言内容不能为空")
    @Length(max = 50, message = "文章内容长度不能超过50")
    private String content;
}Getter + Setter + ToString
dev-model  com/imooc/pojo/Comments.java
public class Comments {
    @Id
    private String id;

    /**
     * 评论的文章是哪个作者的关联id
     */
    @Column(name = "writer_id")
    private String writerId;

    /**
     * 如果是回复留言,则本条为子留言,需要关联查询
     */
    @Column(name = "father_id")
    private String fatherId;

    /**
     * 回复的那个文章id
     */
    @Column(name = "article_id")
    private String articleId;

    /**
     * 冗余文章标题,宽表处理,非规范化的sql思维,对于几百万文章和几百万评论的关联查询来讲,性能肯定不行,所以做宽表处理,从业务角度来说,文章发布以后不能随便修改标题和封面的
     */
    @Column(name = "article_title")
    private String articleTitle;

    /**
     * 文章封面
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 发布留言的用户id
     */
    @Column(name = "comment_user_id")
    private String commentUserId;

    /**
     * 冗余用户昵称,非一致性字段,用户修改昵称后可以不用同步
     */
    @Column(name = "comment_user_nickname")
    private String commentUserNickname;

    /**
     * 冗余的用户头像
     */
    @Column(name = "comment_user_face")
    private String commentUserFace;

    /**
     * 留言内容
     */
    private String content;

    /**
     * 留言时间
     */
    @Column(name = "create_time")
    private Date createTime;

用户评论入库保存【文章评论】这里暂时把数据库的comment_user_face删除了

service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {

    @PostMapping("createComment")
    @ApiOperation(value = "用户评论", notes = "用户评论", httpMethod = "POST") //@Valid是做验证的
    public GraceJSONResult createArticle(@RequestBody @Valid CommentReplyBO commentReplyBO, BindingResult result);
}
service-article  com/imooc/article/controller/CommentController.java
@RestController
public class CommentController extends BaseController implements CommentControllerApi {
    final static Logger logger = LoggerFactory.getLogger(CommentController.class);
    @Autowired
    private CommentPortalService commentPortalService;

    @Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();

        // 3. 保存用户评论的信息到数据库
        commentPortalService.createComment(commentReplyBO.getArticleId(), commentReplyBO.getFatherId(), commentReplyBO.getContent(), userId, nickname);

        return GraceJSONResult.ok();
    }
}
service-article  com/imooc/article/service/CommentPortalService.java
public interface CommentPortalService {
    /**
     * 发表评论
     */
    public void createComment(String articleId,
                              String fatherCommentId,
                              String content,
                              String userId,
                              String nickname);
}
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
package com.imooc.article.service.impl;

import com.imooc.api.service.BaseService;
import com.imooc.article.mapper.CommentsMapper;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.CommentPortalService;
import com.imooc.pojo.Comments;
import com.imooc.pojo.vo.ArticleDetailVO;
import com.imooc.utils.PagedGridResult;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;


@Service
public class CommentPortalServiceImpl extends BaseService implements CommentPortalService {
    @Autowired
    private ArticlePortalService articlePortalService;
    @Autowired
    private Sid sid;
    @Autowired
    private CommentsMapper commentsMapper;

    @Transactional
    @Override
    public void createComment(String articleId, String fatherCommentId, String content, String userId, String nickname) {
        String commentId = sid.nextShort();
        ArticleDetailVO article = articlePortalService.queryDetail(articleId);
        Comments comments = new Comments();
        comments.setId(commentId);

        comments.setWriterId(article.getPublishUserId());
        comments.setArticleTitle(article.getTitle());
        comments.setArticleCover(article.getCover());
        comments.setArticleId(articleId);

        comments.setFatherId(fatherCommentId);
        comments.setCommentUserId(userId);
        comments.setCommentUserNickname(nickname);

        comments.setContent(content);
        comments.setCreateTime(new Date());

        commentsMapper.insert(comments);

        // 评论数累加
        redis.increment(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId, 1);
    }
}
dev-model  com/imooc/pojo/Comments.java
public class Comments {
    @Id
    private String id;

    /**
     * 评论的文章是哪个作者的关联id
     */
    @Column(name = "writer_id")
    private String writerId;

    /**
     * 如果是回复留言,则本条为子留言,需要关联查询
     */
    @Column(name = "father_id")
    private String fatherId;

    /**
     * 回复的那个文章id
     */
    @Column(name = "article_id")
    private String articleId;

    /**
     * 冗余文章标题,宽表处理,非规范化的sql思维,对于几百万文章和几百万评论的关联查询来讲,性能肯定不行,所以做宽表处理,从业务角度来说,文章发布以后不能随便修改标题和封面的
     */
    @Column(name = "article_title")
    private String articleTitle;

    /**
     * 文章封面
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 发布留言的用户id
     */
    @Column(name = "comment_user_id")
    private String commentUserId;

    /**
     * 冗余用户昵称,非一致性字段,用户修改昵称后可以不用同步
     */
    @Column(name = "comment_user_nickname")
    private String commentUserNickname;

//    /**
//     * 冗余的用户头像
//     */
//    @Column(name = "comment_user_face")
//    private String commentUserFace;

    /**
     * 留言内容
     */
    private String content;

    /**
     * 留言时间
     */
    @Column(name = "create_time")
    private Date createTime;
}
service-api  com/imooc/api/BaseController.java
public List<AppUserVO> getBasicUserList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> userVOList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            userVOList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return userVOList;
    }
service-article  com/imooc/article/mapper/CommentsMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Comments;
import org.springframework.stereotype.Repository;

@Repository
public interface CommentsMapper extends MyMapper<Comments> {
}
service-article  resources/mapper/CommentsMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="writer_id" property="writerId" jdbcType="VARCHAR" />
    <result column="father_id" property="fatherId" jdbcType="VARCHAR" />
    <result column="article_id" property="articleId" jdbcType="VARCHAR" />
    <result column="article_title" property="articleTitle" jdbcType="VARCHAR" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="comment_user_id" property="commentUserId" jdbcType="VARCHAR" />
    <result column="comment_user_nickname" property="commentUserNickname" jdbcType="VARCHAR" />
    <result column="content" property="content" jdbcType="VARCHAR" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>

评论数累计与显示【文章评论】

service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {
@GetMapping("counts")
    @ApiOperation(value = "用户评论数查询", notes = "用户评论数查询", httpMethod = "GET")
    public GraceJSONResult commentCounts(@RequestParam String articleId);
}
service-article  com/imooc/article/controller/CommentController.java
 @Override
    public GraceJSONResult commentCounts(String articleId) {
        Integer counts = getCountsFromRedis(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId);
        return GraceJSONResult.ok(counts);
    }

--------------------------------------------------------------------------
service-api  com/imooc/api/BaseController.java
public Integer getCountsFromRedis(String key){
        String countsStr = redis.get(key);
        if (StringUtils.isBlank(countsStr)) {
            countsStr = "0";
        }
        return Integer.valueOf(countsStr);
    }

文章评论sql关联查询father_id…

【多表关联查询】
SELECT
    c.id as commentId,
    c.father_id as fatherId,
    c.comment_user_id as commentUserId,
    c.comment_user_nickname as commentUserNickname,
    c.article_id as articleId,
    c.content as content,
    c.create_time as createTime,
    f.comment_user_nickname as quoteUserNickname,
    f.content as quoteContent
FROM
    comments c
LEFT JOIN
    comments f
ON
    c.father_id = f.id
WHERE
    c.article_id = '2006117B57WRZGHH'
ORDER BY
    c.create_time
DESC

显示评论列表【文章评论】

service-api  com/imooc/api/controller/article/CommentControllerApi.java
com/imooc/api/controller/article/CommentControllerApi.java 
@GetMapping("list")
    @ApiOperation(value = "查询文章的所有评论列表", notes = "查询文章的所有评论列表", httpMethod = "GET")
    public GraceJSONResult list(@RequestParam String articleId,
                                @RequestParam Integer page,
                                @RequestParam Integer pageSize);
service-article  com/imooc/article/controller/CommentController.java
@Override
    public GraceJSONResult list(String articleId, Integer page, Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }
        PagedGridResult gridResult = commentPortalService.queryArticleComments(articleId, page, pageSize);
        return GraceJSONResult.ok(gridResult);
    }
==============================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=200816961ZYBXFRP
service-article  com/imooc/article/service/CommentPortalService.java
/**
     * 查询文章评论列表
     */
    public PagedGridResult queryArticleComments(String articleId,
                                                Integer page,
                                                Integer pageSize);
service-article  com/imooc/article/mapper/CommentsMapperCustom.java
package com.imooc.article.mapper;

import com.imooc.pojo.vo.CommentsVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;

@Repository
public interface CommentsMapperCustom {

    /**
     * 查询文章评论
     */
    public List<CommentsVO> queryArticleCommentList(@Param("paramMap") Map<String, Object> map);

}
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
@Override
    public PagedGridResult queryArticleComments(String articleId, Integer page, Integer pageSize) {
        Map<String, Object> map = new HashMap<>();
        map.put("articleId", articleId);

        PageHelper.startPage(page, pageSize);
        List<CommentsVO> list = commentsMapperCustom.queryArticleCommentList(map);
        return setterPagedGrid(list,page);
    }
service-article  resources/mapper/CommentsMapperCustom.xml #【把关于face的字段都删掉】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapperCustom" >

  <select id="queryArticleCommentList"
          resultType="com.imooc.pojo.vo.CommentsVO"
          parameterType="Map">

    SELECT
      c.id as commentId,
      c.father_id as fatherId,
      c.comment_user_id as commentUserId,
      c.comment_user_nickname as commentUserNickname,
      c.article_id as articleId,
      c.content as content,
      c.create_time as createTime,
      f.comment_user_nickname as quoteUserNickname,
      f.content as quoteContent
    FROM
      comments c
        LEFT JOIN
      comments f
      ON
        c.father_id = f.id
    WHERE
      c.article_id = #{paramMap.articleId}
    ORDER BY
      c.create_time
            DESC

  </select>
</mapper>

(作业) 管理评论列表以及删除评论【评论管理】

前端的commentMng.html的VUE有问题
需要增加定义userInfo
var mainPage = new Vue({
el: “#mainPage”,
data: {
userInfo: {
activeStatus: 0
},
}…

service-api  com/imooc/api/controller/article/CommentControllerApi.java
    @PostMapping("mng")
    @ApiOperation(value = "查询我的评论管理列表", notes = "查询我的评论管理列表", httpMethod = "POST")
    public GraceJSONResult mng(@RequestParam String writerId,
                               @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                               @RequestParam Integer page,
                               @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                               @RequestParam Integer pageSize);


    @PostMapping("/delete")
    @ApiOperation(value = "作者删除评论", notes = "作者删除评论", httpMethod = "POST")
    public GraceJSONResult delete(@RequestParam String writerId,
                                  @RequestParam String commentId);
service-article  com/imooc/article/controller/CommentController.java
 @Override
    public GraceJSONResult mng(String writerId, Integer page, Integer pageSize) {

        if (page == null) {
            page = COMMON_START_PAGE;
        }
        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = commentPortalService.queryWriterCommentsMng(writerId, page, pageSize);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult delete(String writerId, String commentId) {
        commentPortalService.deleteComment(writerId, commentId);
        return GraceJSONResult.ok();
    }
==============================================================
http://writer.imoocnews.com:9090/imooc-news/writer/commentMng.html
service-article  com/imooc/article/service/CommentPortalService.java
     /**
     * 查询我的评论管理列表
     */
    public PagedGridResult queryWriterCommentsMng(String writerId, Integer page, Integer pageSize);

    /**
     * 删除评论
     */
    public void deleteComment(String writerId, String commentId);
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
@Override
    public PagedGridResult queryWriterCommentsMng(String writerId, Integer page, Integer pageSize) {
        Comments comment = new Comments();
        comment.setWriterId(writerId);
        PageHelper.startPage(page, pageSize);
        List<Comments> list = commentsMapper.select(comment);
        return setterPagedGrid(list,page);
    }

    @Override
    public void deleteComment(String writerId, String commentId) {
        Comments comment = new Comments();
        comment.setId(commentId);
        comment.setWriterId(writerId);
        commentsMapper.delete(comment);
    }

增加评论者头像展示功能需求扩展【文章评论】增加需求字段comment_user_face

[数据库添加一个新的字段comment_user_face 重新在mybatis-generator-database进行逆向生成覆盖]
涉及范围广

在数据库里也要加个字段 在前端需求也要改一下头像

detail.html
<div class="all-comments-list" v-for="(comment,index) in commentList" :key="index">
        <div class="single-comment-wrapper">
        <!--<img src="./img/face1.png" class="user-face"/>-->
        <img :src="comment.commentUserFace" class="user-face"/>
</div>
service-article  resources/mapper/CommentsMapper.xml #【增加字段】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="writer_id" property="writerId" jdbcType="VARCHAR" />
    <result column="father_id" property="fatherId" jdbcType="VARCHAR" />
    <result column="article_id" property="articleId" jdbcType="VARCHAR" />
    <result column="article_title" property="articleTitle" jdbcType="VARCHAR" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="comment_user_id" property="commentUserId" jdbcType="VARCHAR" />
    <result column="comment_user_nickname" property="commentUserNickname" jdbcType="VARCHAR" />
    <result column="comment_user_face" property="commentUserFace" jdbcType="VARCHAR" />
    <result column="content" property="content" jdbcType="VARCHAR" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>
service-article  resources/mapper/CommentsMapperCustom.xml #【增加字段】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapperCustom" >

  <select id="queryArticleCommentList"
          resultType="com.imooc.pojo.vo.CommentsVO"
          parameterType="Map">

    SELECT
      c.id as commentId,
      c.father_id as fatherId,
      c.comment_user_id as commentUserId,
      c.comment_user_nickname as commentUserNickname,
      c.comment_user_face as commentUserFace,
      c.article_id as articleId,
      c.content as content,
      c.create_time as createTime,
      f.comment_user_nickname as quoteUserNickname,
      f.content as quoteContent
    FROM
      comments c
        LEFT JOIN
      comments f
      ON
        c.father_id = f.id
    WHERE
      c.article_id = #{paramMap.articleId}
    ORDER BY
      c.create_time
            DESC

  </select>

</mapper>
service-article  com/imooc/article/service/CommentPortalService.java //【增加字段】
 /**
     * 发表评论
     */
    public void createComment(String articleId,
                              String fatherCommentId,
                              String content,
                              String userId,
                              String nickname,
                              String face);
------------------------------------------------------------
dev-model  com/imooc/pojo/vo/CommentsVO.java //【增加字段属性】
private String commentUserFace;
    【Getter + Setter】

------------------------------------------------------------
dev-model  com/imooc/pojo/Comments.java
/**
     * 冗余的用户头像
     */
@Column(name = "comment_user_face")
private String commentUserFace;
    【Getter + Setter】
------------------------------------------------------------
service-article  com/imooc/article/controller/CommentController.java 
    //【增加 String face = getBasicUserList(idSet).get(0).getFace();】
@Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();
        String face = getBasicUserList(idSet).get(0).getFace();

        // 3. 保存用户评论的信息到数据库
        commentPortalService.createComment(commentReplyBO.getArticleId(), commentReplyBO.getFatherId(), commentReplyBO.getContent(), userId, nickname,face);

        return GraceJSONResult.ok();
    }
====================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=2006116Z3MAP8SW0
//下面有个评论:牛逼  带着自己上传的头像
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
    //【增加 comments.setCommentUserFace(face);】
@Transactional
    @Override
    public void createComment(String articleId, String fatherCommentId, String content, String userId, String nickname,String face) {
        String commentId = sid.nextShort();
        ArticleDetailVO article = articlePortalService.queryDetail(articleId);
        Comments comments = new Comments();
        comments.setId(commentId);

        comments.setWriterId(article.getPublishUserId());
        comments.setArticleTitle(article.getTitle());
        comments.setArticleCover(article.getCover());
        comments.setArticleId(articleId);

        comments.setFatherId(fatherCommentId);
        comments.setCommentUserId(userId);
        comments.setCommentUserNickname(nickname);
        comments.setCommentUserFace(face);

        comments.setContent(content);
        comments.setCreateTime(new Date());

        commentsMapper.insert(comments);

        // 评论数累加
        redis.increment(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId, 1);
    }

文章静态化技术与Freemarker【文章概述】

  • 页面静态化
  • Freemarker静态化技术
  • 渲染模板数据
  • 生成并展示静态页面
静态化趋势
  • 便于SEO
  • 加速用户访问
  • 降低数据库压力
模板引擎技术
  • JSP
  • Freemarker
  • Thymeleaf
  • Velocity
页面静态化

创建并且显示模板ftl

service-article  pom.xml
 <!-- freemarker 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }
}
==================================================================
http://localhost:8001/free/hello
service-article  resources/application.yml 【suffix是模板后缀】
  freemarker:
    charset: UTF-8
    content-type: text/html
    suffix: .ftl
    template-loader-path: classpath:/templates/
service-article  resources/templates/stu.ftl
<html>
    <head>
        <title>Hello Freemarker</title>
    </head>
    <body>
    <#-- 
        写完以后去模板页面配置 application.yml
        Freemarker 页面的语法构成:
        1. 注释
        2. 表达式 ${...}
        3. 普通文本,基本的html标签
        4. 指令
    -->
        <div>
            hello ${there}
        </div>
    </body>
</html>

输出对象【Freemarker语法】

dev-model  com/imooc/pojo/Stu.java
public class Stu {
    private String uid;
    private String username;
    private Integer age;
    private Date birthday;
    private Float amount;
    private boolean haveChild;
    private Spouse spouse;
} Getter + Setter
dev-model  com/imooc/pojo/Spouse.java
public class Spouse {
    private String username;
    private Integer age;
} Getter + Setter
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);
        stu.setSpouse(spouse);
        model.addAttribute("stu",stu);
        return model;
    }
}
==================================================================
http://localhost:8001/free/hello

Hello 慕课网 imooc.com

用户名uid: 10010
用户姓名: imooc
年龄:18
生日:2024-07-29 15:13:05
用户余额:88.86
已育:yes
伴侣:Lucy,25岁
service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        伴侣:${stu.spouse.username},${stu.spouse.age}岁

    </div>
</body>
</html>

输出list与map【Freemarker语法】

dev-model  com/imooc/pojo/Stu.java
public class Stu {
    private String uid;
    private String username;
    private Integer age;
    private Date birthday;
    private Float amount;
    private boolean haveChild;

    private Spouse spouse;

    private List<Article> articleList;
    private Map<String, String> parents;
} Getter + Setter
dev-model  com/imooc/pojo/Spouse.java
public class Spouse {
    private String username;
    private Integer age;
} Getter + Setter
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.Spouse;
import com.imooc.pojo.Stu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);

        stu.setSpouse(spouse);
        stu.setArticleList(getArticles());
        stu.setParents(getParents());

        model.addAttribute("stu",stu);
        return model;
    }

    private List<Article> getArticles(){
        Article article1 = new Article();
        article1.setId("1001");
        article1.setTitle("今天天气不错");

        Article article2 = new Article();
        article2.setId("1002");
        article2.setTitle("今天下雨了");

        Article article3 = new Article();
        article3.setId("1003");
        article3.setTitle("昨天下雨了");

        List<Article> list = new ArrayList<>();
        list.add(article1);
        list.add(article2);
        list.add(article3);
        return list;
    }

    private Map<String, String> getParents(){
        Map<String, String> parents = new HashMap<>();
        parents.put("father", "XiaoMing");
        parents.put("mother", "LiLi");
        return parents;
    }
}
==================================================================
http://localhost:8001/free/hello

hello 慕课网 imooc.com

用户名uid: 10010
用户姓名: imooc
年龄:18
生日:2024-07-29 15:39:07
用户余额:88.86
已育:yes
伴侣:Lucy,25岁

1001 今天天气不错
1002 今天下雨了
1003 昨天下雨了

LiLi
XiaoMing
service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        伴侣:${stu.spouse.username},${stu.spouse.age}岁
    </div>

<br>

    <div>
        <#list stu.articleList as article>
            <div>
                <span>${article.id}</span>
                <span>${article.title}</span>
            </div>
        </#list>
    </div>

<br>

    <div>
        <#list stu.parents?keys as key>
            <div>
                ${stu.parents[key]}
            </div>
        </#list>
    </div>
</body>
</html>

指令if【Freemarker语法】

service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 
    写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        <#if stu.spouse??>
            伴侣:${stu.spouse.username}, ${stu.spouse.age}岁
        </#if>
        <#if !stu.spouse??>
            单身狗
        </#if>
    </div>

<br>

    <div>
        <#list stu.articleList as article>
            <div>
                <span>${article.id}</span>
                <span>${article.title}</span>
            </div>
        </#list>
    </div>

<br>

    <div>
        <#list stu.parents?keys as key>
            <div>
                ${stu.parents[key]}
            </div>
        </#list>
    </div>

<br>

    <div>
        <#if stu.uid == '10010'>
            用户id是10010
            <br>
        </#if>
        <#if stu.username != 'imooc'>
            用户名不是imooc
            <br>
        </#if>
        <#if (stu.age >= 18) >
            用户已成年
        </#if>
        <br>
        <#if (stu.age > 18 || stu.age = 18) >
            成年人
            <br>
        </#if>
        <#if (stu.age < 18) >
            未成年
            <br>
        </#if>
        <#if stu.haveChild >
            已育
        </#if>
        <br>
        <#if !stu.haveChild >
             未育
        </#if>
     </div>
</body>
</html>

在这里特别注意一下 已经开始第二阶段的代码 进阶篇 所以前端的代码也是需要更新换代的 包括../js/app.js里面多了 app.getPageName();
没有getPageName这个函数-慕课网 (imooc.com)
生成的html调用app.js中getPageName()函数出错的问题-慕课网 (imooc.com)

结合动态数据生成静态化HTML【Freemarker】

service-article  com/imooc/article/controller/FreemarkerController.java
【stu.ftl如上图不变增加java的整合代码】 俗称Java+ftl=HTML
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.Spouse;
import com.imooc.pojo.Stu;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.*;

@Controller
@RequestMapping("free")
public class FreemarkerController{

    @Value("${freemarker.html.target}")
    private String htmlTarget;

    @GetMapping("/createHTML")
    @ResponseBody
    public String createHTML(Model model) throws IOException, TemplateException {
        // 0. 配置freemarker基本环境
        Configuration cfg = new Configuration(Configuration.getVersion());
        // 声明freemarker模板所需要加载的目录的位置
            //resources/templates/stu.ftl
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File((classpath + "templates")));

            // 测试打印
        System.out.println(htmlTarget);
        System.out.println(classpath + "templates");
        /**
         * /workspace/freemarker_html
         * /C:/Users/Pluminary/Desktop/backup/imooc-news-dev/imooc-news-dev-service-article/target/classes/templates
         */
        // 1. 获得现有的模板ftl文件
        Template template = cfg.getTemplate("stu.ftl", "utf-8");

        // 2. 获得动态数据
            // 定义输出到模板的内容
            // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);
        model = makeModel(model);

        // 3. 融合动态数据和ftl,生成html
        File tempDic = new File(htmlTarget);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }
        Writer out = new FileWriter(htmlTarget + File.separator + "10010" + ".html");
        template.process(model, out);
        out.close();
        return "ok";
        // C:\workspace\freemarker_html\10010.html 里面的数据都是静态数据
    }

    @GetMapping("/hello")
    public String hello(Model model){
        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);

        stu.setSpouse(spouse);
        stu.setArticleList(getArticles());
        stu.setParents(getParents());

        model.addAttribute("stu",stu);
        return model;
    }

    private List<Article> getArticles(){
        Article article1 = new Article();
        article1.setId("1001");
        article1.setTitle("今天天气不错");

        Article article2 = new Article();
        article2.setId("1002");
        article2.setTitle("今天下雨了");

        Article article3 = new Article();
        article3.setId("1003");
        article3.setTitle("昨天下雨了");

        List<Article> list = new ArrayList<>();
        list.add(article1);
        list.add(article2);
        list.add(article3);
        return list;
    }

    private Map<String, String> getParents(){
        Map<String, String> parents = new HashMap<>();
        parents.put("father", "XiaoMing");
        parents.put("mother", "LiLi");
        return parents;
    }
}

改写详情页为模板页ftl【页面静态化】

地址页不是拼接 将detail.html 的路径都变成绝对路径

<link rel="shortcut icon" href="../img/mu-toutiao.ico" />

把其拷贝到 resources/templates/detail.ftl

【放在d盘 tomcat/webapps中的】detail.ftl
<div class="big-title">
     ${articleDetail.title}
</div>
    <div class="read-counts" v-show="articleDetail.readCounts != '' && articleDetail.readCounts != null">
     阅读量:${articleDetail.readCounts}
    </div>
......
<div class="date-title">
       <span class="year">${articleDetail.publishTime?string('yyyy')}</span>
 </div>
      <div class="back-year-line"></div>

      <div class="date-md">${articleDetail.publishTime?string('MM/dd')}</div>

      <div class="date-times">${articleDetail.publishTime?string('HH:mm:ss')}</div>

      <div class="writer-name" @click="showWriter('${articleDetail.publishUserId}')">
                        ${articleDetail.publishUserName}
                    </div>
.....
<div class="article-wrapper">
   <div class="content">
        ${articleDetail.content}
   </div>
<div class="declare">
      免责声明:本平台所有内容仅供测试,且文章来自互联网,不代表慕课网的观点和立场,如有不妥,请联系后删除。
    </div>
</div>

文章详情ftl生成静态化页面【页面静态化】

freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
dev-model  com/imooc/pojo/vo/ArticleDetailVO.java
public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter
service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);
        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
                 createArticleHTML(articleId);
//                String articleMongoId = createArticleHTMLToGridF(articleId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }

    @Value("${freemarker.html.target}")
    private String articlePath;
    @Autowired
    private RestTemplate restTemplate;
    // 文章生成HTML
    public void createArticleHTML(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        File tempDic = new File(articlePath);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }

        String path = articlePath + File.separator + detailVO.getId() + ".html";

        Writer out = new FileWriter(path);
        template.process(map, out);
        out.close();
    }
    // 发起远程调用rest,获得文章详情数据
    public ArticleDetailVO getArticleDetail(String articleId) {
        String url
                = "http://www.imoocnews.com:8001/portal/article/detail?articleId=" + articleId;
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(url, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        ArticleDetailVO detailVO = null;
        if (bodyResult.getStatus() == 200) {
            String detailJson = JsonUtils.objectToJson(bodyResult.getData());
            detailVO = JsonUtils.jsonToPojo(detailJson, ArticleDetailVO.class);
        }
        return detailVO;
    }
先去发表头条http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html
再去审核通过http://www.imoocnews.com:9090/imooc-news/admin/contentReview.html
此时运行后 就会有java+ftl=html静态页面在指定位置生成了
指定位置:D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\a
生成了一个文件:240729D9S8683XP0.html  这里面就是刚刚发表头条的内容
此时下面的两个网站都可以打开同样的头条内容
http://www.imoocnews.com:9090/imooc-news/portal/a/240729D9S8683XP0.html
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240729D9S8683XP0

240729D9S8683XP0.html
<div class="writer-name" @click="showWriter('240629F21AK1BHX4')">
      P_luminary
</div>

// 跳转作家页面
showWriter(writerId) {
    window.open("../writer.html?writerId=" + writerId);
},

没有getPageName这个函数-慕课网 (imooc.com)

文章阅读量detail单独获取并展示 【页面静态化】

a5 (imoocnews.com)

【去前面代入阅读量】
 <div class="read-counts">
            阅读量:{{readCounts}}
        </div>
【先定义readCounts初始量为0】
var articleList = new Vue({
        el: "#detailContainer",
        data: {
            nowReplyingFatherCommentId: 0,  // 根据当前用户正在回复的父commentId进行页面的留言看展示或隐藏
            userInfo: null,
            
            articleId: "",
            articleDetail: {},
            readCounts: 0,
        }

// 获得文章阅读数
         this.getArticleReadCounts(articleId);

 // 获得文章阅读数
        getArticleReadCounts(articleId) {
               var me = this;

               var articleServerUrl = app.articleServerUrl;
               axios.defaults.withCredentials = true;
               axios.get(articleServerUrl + "/portal/article/readCounts?articleId=" + articleId)
                .then(res => {
                  // console.log(JSON.stringify(res.data));
                  this.readCounts = res.data;
                });
            },
//★★★★★★★★★★★★★★★★★★★★     ★★★★★★★★★★★★★★★★★★★★★★★\\
然后把这个临时页面修改的地方 移动到后端service-article  resources/templates/detail.ftl中
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public Integer readCounts(String articleId) {
       return getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId);
    }
service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@GetMapping("readCounts")
    @ApiOperation(value = "获得文章阅读数", notes = "获得文章阅读数", httpMethod = "GET")
    public Integer readCounts(@RequestParam String articleId);

在这里折腾一天终于好了 文章:a6 (imoocnews.com)

9-10

梳理生产端消费端与中间gridfs关系

静态化高度耦合

在本地电脑/同一台服务器 => 生成静态页面HTML →(发布) 前端

解耦静态化

后端服务器(生成静态页面HTML) => GridFS <= 前端服务器(前端HTML)
同时 后端服务器(生成静态页面HTML) → 前端服务器(前端HTML)

创建一个新的module => imooc-news-dev-service-article-html
把service-article中resources的application.yml /dev+prod.yml logback-spring.xml拷贝到article-html模块的resources中

############################################################
http://localhost:8002/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}
application.yml
############################################################
#
# article文章静态化服务
# web访问端口号  约定:8002
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-article-html
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
  freemarker:
    charset: UTF-8
    content-type: text/html
    suffix: .ftl
    template-loader-path: classpath:/templates/

# 定义freemarker生成的HTML
freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
application-dev.yml
server:
  port: 8002

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

## setup CN from java, This is resource
website:
  domain-name: imoocnews.com




application-prod.yml
server:
  port: 8002

spring:
  redis:
    database: 0
    host: 47.98.225.105
    port: 6379
service-article-html  com/imooc/article/html/Application.java
package com.imooc.article.html;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-article-html  com/imooc/article/html/controller/HelloController.java
package com.imooc.article.html.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}

生产端存储html道gridfs并关联文章表【静态化解耦】

静态化解耦步骤
  • 生成html,并上传到gridfs中
  • 获得mongoFileId,关联保存到文章表中
  • 调用消费端,下载gridfs的html进行发布
service-article  com/imooc/article/service/ArticleService.java
 /**
     * 关联文章和gridfs的html文件id
     */
    public void updateArticleToGridFS(String articleId, String articleMongoId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
 @Transactional
    @Override
    public void updateArticleToGridFS(String articleId, String articleMongoId) {
        Article pendingArticle = new Article();
        pendingArticle.setId(articleId);
        pendingArticle.setMongoFileId(articleMongoId);
        articleMapper.updateByPrimaryKeySelective(pendingArticle);
    }
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
......
...
    @Value("${freemarker.html.target}")
    private String articlePath;
    @Autowired
    private RestTemplate restTemplate;
    // 文章生成HTML
    public void createArticleHTML(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        File tempDic = new File(articlePath);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }

        String path = articlePath + File.separator + detailVO.getId() + ".html";

        Writer out = new FileWriter(path);
        template.process(map, out);
        out.close();
    }

    @Autowired
    private GridFSBucket gridFSBucket;
    public String createArticleHTMLToGridFS(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
//        System.out.println(htmlContent);

        InputStream inputStream = IOUtils.toInputStream(htmlContent);
        ObjectId fileId = gridFSBucket.uploadFromStream(detailVO.getId() + ".html",inputStream);
        return fileId.toString();
    }
service-article  com/imooc/article/GridFSConfig.java
package com.imooc.article;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component //可以被容器访问到
public class GridFSConfig {
    @Value("${spring.data.mongodb.database}")
    private String mongodb;

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient){
        MongoDatabase mongoDatabase = mongoClient.getDatabase(mongodb);
        GridFSBucket bucket = GridFSBuckets.create(mongoDatabase);//存入mongodatabase
        return bucket;
    }
}

消费端从gridfs下载HTML到tomcat【静态化解耦】

service-api  com/imooc/api/controller/article/ArticleHTMLControllerApi.java
package com.imooc.api.controller.article;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewArticleBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Date;

@Api(value = "静态化文章业务的controller", tags = {"静态化文章业务的controller"})
@RequestMapping("article/html")
public interface ArticleHTMLControllerApi {

    @GetMapping("download")
    @ApiOperation(value = "下载html", notes = "下载html", httpMethod = "GET")
    public Integer download(String articleId, String articleMongoId) throws Exception;
}
article-html  com/imooc/article/html/controller/ArticleHTMLController.java
package com.imooc.article.html.controller;

import com.imooc.api.controller.article.ArticleHTMLControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.gridfs.GridFS;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

@RestController
public class ArticleHTMLController implements ArticleHTMLControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticleHTMLController.class);

    @Autowired //相应的下载
    private GridFSBucket gridFSBucket;

    @Value("${freemarker.html.article}")
    private String articlePath;

    @Override
    public Integer download(String articleId, String articleMongoId)
            throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 创建输出流
        OutputStream outputStream = new FileOutputStream(file);
        // 执行下载
        gridFSBucket.downloadToStream(new ObjectId(articleMongoId), outputStream);

        return HttpStatus.OK.value();
    }
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
                // 调用消费端,执行下载html
                doDownloadArticleHTML(articleId,articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
    private void doDownloadArticleHTML(String articleId, String articleMongoId) {
        String url = //去SwitchHost弄个新的端口映射
                "http://html.imoocnews.com:8002/article/html/download?articleId="
                        + articleId +
                        "&articleMongoId="
                        + articleMongoId;
        ResponseEntity<Integer> responseEntity = restTemplate.getForEntity(url, Integer.class);
        int status = responseEntity.getBody();
        if (status != HttpStatus.OK.value()) {
            GraceException.display(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
    }
service-article  com/imooc/article/service/ArticleService.java
      /**
     * 关联文章和gridfs的html文件id
     */
    public void updateArticleToGridFS(String articleId, String articleMongoId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
    @Transactional
    @Override
    public void updateArticleToGridFS(String articleId, String articleMongoId) {
        Article pendingArticle = new Article();
        pendingArticle.setId(articleId);
        pendingArticle.setMongoFileId(articleMongoId);
        articleMapper.updateByPrimaryKeySelective(pendingArticle);
    }
【SwitchHosts】
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com

发布文章后审核文章
此时会发现数据库MongoDB里面的GridFS存储桶有新建的html 包括在 前端也存在此文件
D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\a\240731CN3X1M56Y8.html

撤回删除文章,删除gridfs文件以及html【静态化解耦】

拿到mongodb_id 去删除 在service-html 写个删除接口 拼接删除方法

service-api  com/imooc/api/controller/article/ArticleHTMLControllerApi.java
@Api(value = "静态化文章业务的controller", tags = {"静态化文章业务的controller"})
@RequestMapping("article/html")
public interface ArticleHTMLControllerApi {
@GetMapping("delete")
    @ApiOperation(value = "删除html", notes = "删除html", httpMethod = "GET")
    public Integer delete(String articleId) throws Exception;
}
article-html  com/imooc/article/html/controller/ArticleHTMLController.java
@Override
    public Integer delete(String articleId) throws Exception {
        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";
        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 删除文件
        file.delete();
        return HttpStatus.OK.value();
    }
service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult delete(String userId, String articleId) {
        articleService.deleteArticle(userId,articleId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult withdraw(String userId, String articleId) {
        articleService.withdrawArticle(userId, articleId);
        return GraceJSONResult.ok();
    }
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Transactional
    @Override
    public void deleteArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setIsDelete(YesOrNo.YES.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_DELETE_ERROR);
        }
        deleteHTML(articleId);
    }

    @Transactional
    @Override
    public void withdrawArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setArticleStatus(ArticleReviewStatus.WITHDRAW.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_WITHDRAW_ERROR);
        }
        deleteHTML(articleId);
    }
...
...

    @Autowired
    private GridFSBucket gridFSBucket;
    /**
     * 文章撤回删除后,删除静态化的html
     */

    public void deleteHTML(String articleId) {
        // 1. 查询文章的mongoFileId
        Article pending = articleMapper.selectByPrimaryKey(articleId);
        String articleMongoId = pending.getMongoFileId();

        // 2. 删除GridFS上的文件
        gridFSBucket.delete(new ObjectId(articleMongoId));

        // 3. 删除消费端的HTML文件
        doDeleteArticleHTML(articleId);
//        doDeleteArticleHTMLByMQ(articleId);
    }

    @Autowired
    public RestTemplate restTemplate;
    private void doDeleteArticleHTML(String articleId) {
        String url = "http://html.imoocnews.com:8002/article/html/delete?articleId=" + articleId;
        ResponseEntity<Integer> responseEntity = restTemplate.getForEntity(url, Integer.class);
        int status = responseEntity.getBody();
        if (status != HttpStatus.OK.value()) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
    }

接口解耦需求【章节概述】

  • 介绍RabbitMQ
  • RabbitMQ术语
  • 安装与配置消息队列
  • 实现接口调用解耦

RabbitMQ概述_MQ模型

消息队列
  • RabbitMQ
  • ActiveMQ
  • RocketMQ
  • Kafka
RabbitMQ
应用场景
  • 异步任务
  • 提速
  • 接口解耦
  • 削峰
RabbitMQ模型

RabbitMQ-3.8.5 安装与配置详细在”多线程与分布式.md“中有

E:\Java实例项目1-20套\第04套【项目实战】Spring Cloud分布式微服务实战,打造大型自媒体3大业务平台 分布式前后端分离项目分层聚合 养成应对复杂业务的综合技术能力\imooc-news\rabbitmq-server-3.8.5

rabbitmq/erlang - Installation · packagecloud- Bash Scripts

[imooc@imooc ~]$ curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
[imooc@imooc ~]$ sudo yum install erlang
[imooc@imooc ~]$ erl
Erlang/OTP 23 [erts-11.2.2.10] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]

Eshell V11.2.2.10  (abort with ^G)
1> 
[imooc@imooc ~]$ yum list | grep erlang
erlang.x86_64                               23.3.4.11-1.el7            @rabbitmq_erlang
erlang-debuginfo.x86_64                     23.3.4.11-1.el7            rabbitmq_erlang
[imooc@imooc ~]$ sudo rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
[imooc@imooc ~]$ sudo rpm --import https://packagecloud.io/gpg.key

将资源包里的文件拷贝过来 rabbitmq.conf 和 rabbitmq-server.rpm
#[先把两个依赖搞好 => 一个是key 一个是依赖]
[imooc@imooc ~]$ sudo rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
[imooc@imooc ~]$ sudo yum install socat
[imooc@imooc ~]$ sudo rpm -ivh rabbitmq-server-3.8.5-1.el7.noarch.rpm
[imooc@imooc ~]$ sudo vim rabbitmq.conf
#{loopback_users, []} 加上注释#
[imooc@imooc ~]$ cd /etc/rabbitmq/
#把conf移动到etc中
[imooc@imooc rabbitmq]$ sudo cp /home/imooc/rabbitmq.conf .
#重新启动rabbitmq
[imooc@imooc rabbitmq]$ sudo systemctl restart rabbitmq-server
#查看状态
[imooc@imooc rabbitmq]$ sudo systemctl status rabbitmq-server

● rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
   Active: active (running) since 四 2024-08-01 15:45:54 CST; 13s ago
 Main PID: 5843 (beam.smp)
   Status: "Initialized"
    Tasks: 86
   CGroup: /system.slice/rabbitmq-server.service
           ├─5843 /usr/lib64/erlang/erts-11.2.2.10/bin/beam.smp -W w -K true -A 64 -M...
           ├─5952 erl_child_setup 32768
           ├─6008 inet_gethost 4
           └─6009 inet_gethost 4
           
[imooc@imooc rabbitmq]$ sudo rabbitmq-plugins enable rabbitmq_management
[imooc@imooc rabbitmq]$ ll
总用量 40
-rw-r--r--. 1 root rabbitmq    23 8月   1 15:46 enabled_plugins
-rw-r--r--. 1 root rabbitmq 33325 8月   1 15:35 rabbitmq.conf

http://192.168.170.135:15672/
username: guest
password: guest
[imooc@imooc ~]$ sudo vim rabbitmq.conf #把这个取消注释
loopback_users.guest = false

http://192.168.170.135:15672/#/  #创建虚拟host节点
→ admin → Add a user

引入依赖和配置【集成Rabbitmq】

service-api  pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

模块 Module
imooc-news-dev-service-article 是生产者[发送消息]

imooc-news-dev-service-article-html 是消费者[处理消息]

RabbitMQ Management 在Virtual Hosts → Add a new virtual host → Name: imooc-news-dev
退出再重新登陆一下rabbitmq → 账号密码:admin

service-article  application.yml
  rabbitmq:
    host: 192.168.170.135
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev
service-article-html  application.yml
  rabbitmq:
    host: 192.168.170.135
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

创建交换机和队列【集成Rabbitmq】

service-api  com/imooc/api/config/RabbitMQConfig.java
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_ARTICLE = "exchange_article";

    // 定义队列的名字
    public static final String QUEUE_DOWNLOAD_HTML = "queue_download_html";

    // 创建交换机
    @Bean(EXCHANGE_ARTICLE)
    public Exchange exchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_ARTICLE)
                .durable(true)
                .build();
    }

    // 创建队列
    @Bean(QUEUE_DOWNLOAD_HTML)
    public Queue queue(){
        return new Queue(QUEUE_DOWNLOAD_HTML);
    }

    // 队列绑定交换机
    @Bean
    public Binding binding(
            @Qualifier(QUEUE_DOWNLOAD_HTML) Queue queue,
            @Qualifier(EXCHANGE_ARTICLE) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                //.with("article.*") "article.hello",  //类似于API的规则
                .with("article.#.do")
                .noargs();      // 执行绑定
    }
}

创建生产者_配置路由规则【集成RabbitMQ】

localhost:8001/producer/hello

{
“status”: 200,
“msg”: “操作成功!”,
“success”: true,
“data”: null
}

//如果队列规则改变 就需要把Exchanges里的RoutingKey解绑[Unbind] 否则还是会有以前的规则收到消息
//http://192.168.170.135:15672/#/exchanges/imooc-news-dev/exchange_article 
package com.imooc.article.controller;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("producer")
public class HelloController{
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
    /**
     * RabbitMQ的路由规则 routing key
     * display.*.*  →  * 代表一个占位符
     * .with("article.#.do")  //类似于API的规则
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done 不匹配
     *
     * display.# → # 代表任意多个占位符
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done.over 匹配
     */

        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.publish.download.do", //要绑定规则
                "1001~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.success.do", //要绑定规则
                "1002~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.play", //要绑定规则
                "1003~");
        return GraceJSONResult.ok();
    }
}
http://localhost:8001/producer/hello

消费者接受消息处理业务【集成RabbitMQ】

RabbitMQ Management

如果消息被消费掉后那么就 需要重新请求消息队列生成

service-article-html  com/imooc/article/html/RabbitMQConsumer.java
package com.imooc.article.html;

import com.imooc.api.config.RabbitMQConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
//打断点后 只要队列有消息 消费者监听到就会被消费
@Component
public class RabbitMQConsumer {
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})//监听哪个队列
    public void watchQueue(String payload, Message message){
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
            System.out.println("article.publish.download.do");
        } else if (routingKey.equalsIgnoreCase("article.success.do")) {
            System.out.println("article.success.do");
        } else {
            System.out.println("不符合的规则:" + routingKey);
        }
    }
}
==================================================================
// 如果消息被消费掉后那么就 需要重新请求消息队列生成
// 此时需要刷新 http://localhost:8001/producer/hello 重新提交一下消息就可以了
Console:
1001~
article.publish.download.do
1002~
article.success.do
1003~
不符合的规则:article.play

文章静态化HTML与删除【异步解耦】

service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
                // 调用消费端,执行下载html
//                doDownloadArticleHTML(articleId,articleMongoId);
            ★★  // 发送消息到mq队列,让消费者监听并且下载html  ★★
                doDownloadArticleHTMLByMQ(articleId,articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
@Autowired
    private RabbitTemplate rabbitTemplate;
    private void doDownloadArticleHTMLByMQ(String articleId, String articleMongoId) {

        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.download.do",
                articleId + "," + articleMongoId);
    }
service-article-html  com/imooc/article/html/controller/ArticleHTMLComponent.java
package com.imooc.article.html.controller;

import com.mongodb.client.gridfs.GridFSBucket;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

@Component
public class ArticleHTMLComponent {

    @Autowired
    private GridFSBucket gridFSBucket;

    @Value("${freemarker.html.article}")
    private String articlePath;

    public Integer download(String articleId, String articleMongoId)
            throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 创建输出流
        OutputStream outputStream = new FileOutputStream(file);
        // 执行下载
        gridFSBucket.downloadToStream(new ObjectId(articleMongoId), outputStream);

        return HttpStatus.OK.value();
    }

    public Integer delete(String articleId) throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);

        // 删除文件
        file.delete();

        return HttpStatus.OK.value();
    }
}
service-article-html  com/imooc/article/html/RabbitMQConsumer.java
package com.imooc.article.html;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.article.html.controller.ArticleHTMLComponent;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
//打断点后 只要队列有消息 消费者监听到就会被消费
@Component
public class RabbitMQConsumer {
    @Autowired
    private ArticleHTMLComponent articleHTMLComponent;

    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})//监听哪个队列
    public void watchQueue(String payload, Message message){
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
            System.out.println("article.publish.download.do");
        } else if (routingKey.equalsIgnoreCase("article.success.do")) {
            System.out.println("article.success.do");
        }else if (routingKey.equalsIgnoreCase("article.download.do")) {
            String articleId = payload.split(",")[0];
            String articleMongoId = payload.split(",")[1];
            try {
                articleHTMLComponent.download(articleId, articleMongoId);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else if (routingKey.equalsIgnoreCase("article.html.download.do")) {
            String articleId = payload;
            try {
                articleHTMLComponent.delete(articleId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("不符合的规则:" + routingKey);
        }
    }
}
前端的index.html页面也需要修改成静态页面跳转
<a :href="'./a/'+article.id+'.html'" target="_blank" class="link-article-title">{{article.title}}</a>
这样再次刷新 就可以让页面不是articleId=?...
http://www.imoocnews.com:9090/imooc-news/portal/a/240801D7S7PM63R4.html

延迟队列的需求与安装配置【延迟队列】

把这个rabbitmq_delayed_message_exchange-3.8.0.ez上传到Linux虚拟机

[imooc@imooc rabbitmq]$ cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.5/plugins
[imooc@imooc ~]$ sudo mv /home/imooc/rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.5/plugins
[imooc@imooc plugins]$ sudo systemctl restart rabbitmq-server
[imooc@imooc plugins]$ sudo rabbitmq-plugins list
Listing plugins with pattern ".*" ...
 Configured: E = explicitly enabled; e = implicitly enabled
 | Status: * = running on rabbit@imooc
 |/
[  ] rabbitmq_amqp1_0                  3.8.5
[  ] rabbitmq_auth_backend_cache       3.8.5
[  ] rabbitmq_auth_backend_http        3.8.5
[  ] rabbitmq_auth_backend_ldap        3.8.5
[  ] rabbitmq_auth_backend_oauth2      3.8.5
[  ] rabbitmq_auth_mechanism_ssl       3.8.5
[  ] rabbitmq_consistent_hash_exchange 3.8.5
[  ] rabbitmq_delayed_message_exchange 3.8.0
[  ] rabbitmq_event_exchange           3.8.5
[  ] rabbitmq_federation               3.8.5
[  ] rabbitmq_federation_management    3.8.5
[  ] rabbitmq_jms_topic_exchange       3.8.5
[E*] rabbitmq_management               3.8.5
[e*] rabbitmq_management_agent         3.8.5
[  ] rabbitmq_mqtt                     3.8.5
[  ] rabbitmq_peer_discovery_aws       3.8.5
[  ] rabbitmq_peer_discovery_common    3.8.5
[  ] rabbitmq_peer_discovery_consul    3.8.5
[  ] rabbitmq_peer_discovery_etcd      3.8.5
[  ] rabbitmq_peer_discovery_k8s       3.8.5
[  ] rabbitmq_prometheus               3.8.5
[  ] rabbitmq_random_exchange          3.8.5
[  ] rabbitmq_recent_history_exchange  3.8.5
[  ] rabbitmq_sharding                 3.8.5
[  ] rabbitmq_shovel                   3.8.5
[  ] rabbitmq_shovel_management        3.8.5
[  ] rabbitmq_stomp                    3.8.5
[  ] rabbitmq_top                      3.8.5
[  ] rabbitmq_tracing                  3.8.5
[  ] rabbitmq_trust_store              3.8.5
[e*] rabbitmq_web_dispatch             3.8.5
[  ] rabbitmq_web_mqtt                 3.8.5
[  ] rabbitmq_web_mqtt_examples        3.8.5
[  ] rabbitmq_web_stomp                3.8.5
[  ] rabbitmq_web_stomp_examples       3.8.5
[imooc@imooc plugins]$ service rabbitmq-server restart #[重启服务]
Redirecting to /bin/systemctl restart rabbitmq-server.service

实现延迟队列【延迟队列】

service-api  com/imooc/api/config/RabbitMQDelayConfig.java
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQDelayConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    // 定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    // 创建延迟交换机
    @Bean(EXCHANGE_DELAY)
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(EXCHANGE_DELAY, "x-delayed-message", true, false, args);
    }

    // 创建队列
    @Bean(QUEUE_DELAY)
    public Queue queue(){
        return new Queue(QUEUE_DELAY);
    }

    // 队列绑定交换机 ↓ binding必须要唯一
    @Bean
    public Binding delayBinding(
            @Qualifier(QUEUE_DELAY) Queue queue,
            @Qualifier(EXCHANGE_DELAY) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("delay.#")
                .noargs();      // 执行绑定
    }
}
service-article  com/imooc/article/RabbitMQDelayConsumer.java
package com.imooc.article;

import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class RabbitMQDelayConsumer {

    @Autowired
    private ArticleService articleService;

    @RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);

        System.out.println("消费者接受的延迟消息:" + new Date());

//        // 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
//        String articleId = payload;
//        articleService.updateArticleToPublish(articleId);
    }
}
service-article  com/imooc/article/controller/HelloController.java
package com.imooc.article.controller;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@RequestMapping("producer")
public class HelloController{
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
    /**
     * RabbitMQ的路由规则 routing key
     * display.*.*  →  * 代表一个占位符
     * .with("article.#.do")  //类似于API的规则
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done 不匹配
     *
     * display.# → # 代表任意多个占位符
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done.over 匹配
     */

        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.publish.download.do", //要绑定规则
                "1001~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.success.do", //要绑定规则
                "1002~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.play", //要绑定规则
                "1003~");
        return GraceJSONResult.ok();
    }

    @GetMapping("/delay")
    public Object delay() {
        //重写延迟方法  【生产者】
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的持久
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                // 设置消息延迟的时间 单位ms毫秒
                message.getMessageProperties().setDelay(5000);
                return message;
            }
        };
        rabbitTemplate.convertAndSend(RabbitMQDelayConfig.EXCHANGE_DELAY,
                "delay.demo", //要绑定规则
                "这是一条延时消息~",
                messagePostProcessor);
        System.out.println("生产者发送的延迟消息:" + new Date());
        return "OK";
    }
}
确保 rabbitmq_delayed_message_exchange 插件正确启用:

复制代码
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
sudo systemctl restart rabbitmq-server

======================================================================
http://localhost:8001/producer/delay

生产者发送的延迟消息:Thu Aug 01 20:17:30 CST 2024

这是一条延时消息~
delay.demo
消费者接受的延迟消息:Thu Aug 01 20:17:35 CST 2024

实现文章的定时延时发布【延迟队列】

service-article  com/imooc/article/service/ArticleService.java
/**
     * 更新定时发布为即使发布
     
    public void updateAppointToPublish(); **/

    /**
     * 更新单条文章为记时发布
     */
    public void updateArticleToPublish(String articleId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
【32-64行  89-101行】
 @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }

        // 发送延迟消息到mq,计算定时发布时间和当前时间的时间差,则为往后延迟的时间
        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {

            Date endDate = newArticleBO.getPublishTime();
            Date startDate = new Date();

          int delayTimes = (int)(endDate.getTime() - startDate.getTime());

            System.out.println(DateUtil.timeBetween(startDate, endDate));

            // FIXME: 为了测试方便,写死10s
//            int delayTimes = 10 * 1000;

            MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    // 设置消息的持久
                    message.getMessageProperties()
                            .setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                    // 设置消息延迟的时间,单位ms毫秒
                    message.getMessageProperties()
                            .setDelay(delayTimes);
                    return message;
                }
            };
            rabbitTemplate.convertAndSend(
                    RabbitMQDelayConfig.EXCHANGE_DELAY,
                    "publish.delay.display",
                    articleId,
                    messagePostProcessor);

            System.out.println("延迟消息-定时发布文章:" + new Date());
        }


        /**
         * FIXME: 我们只检测正常的词汇,非正常词汇大家课后去检测
         */
        // 通过阿里智能AI实现对文章文本的自动检测(自动审核)
//        String reviewTextResult = aliTextReviewUtils.reviewTextContent(newArticleBO.getContent());
        String reviewTextResult = ArticleReviewLevel.REVIEW.type;

        if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.PASS.type)) {
            // 修改当前的文章,状态标记为审核通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.SUCCESS.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.REVIEW.type)) {
            // 修改当前的文章,状态标记为需要人工审核
            this.updateArticleStatus(articleId, ArticleReviewStatus.WAITING_MANUAL.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.BLOCK.type)) {
            // 修改当前的文章,状态标记为审核未通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.FAILED.type);
        }
    }

@Transactional //添加事务[更新操作]
/** @Override
    public void updateAppointToPublish() {
        articleMapperCustom.updateAppointToPublish();
    } **/

    @Override
    public void updateArticleToPublish(String articleId) {
        Article article = new Article();
        article.setId(articleId);
        article.setIsAppoint(ArticleAppointType.IMMEDIATELY.type);
        articleMapper.updateByPrimaryKeySelective(article);
    }
service-article  com/imooc/article/RabbitMQDelayConsumer.java
package com.imooc.article;

import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class RabbitMQDelayConsumer {

    @Autowired
    private ArticleService articleService;

    @RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);

        System.out.println("消费者接受的延迟消息:" + new Date());

        // 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
        String articleId = payload;
        articleService.updateArticleToPublish(articleId);
    }
}
service-api  com/imooc/api/config/RabbitMQDelayConfig.java //【换一下绑定类型.with(...)】
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQDelayConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    // 定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    // 创建延迟交换机
    @Bean(EXCHANGE_DELAY)
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(EXCHANGE_DELAY, "x-delayed-message", true, false, args);
    }

    // 创建队列
    @Bean(QUEUE_DELAY)
    public Queue queue(){
        return new Queue(QUEUE_DELAY);
    }

    // 队列绑定交换机 ↓ binding必须要唯一
    @Bean
    public Binding delayBinding(
            @Qualifier(QUEUE_DELAY) Queue queue,
            @Qualifier(EXCHANGE_DELAY) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("publish.delay.#")
                .noargs();      // 执行绑定
    }
}
http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html 发布一篇定时文章
// 在数据库里面是article → is_appoint 是1 然后延迟3天后会变成0
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472]
JDBC Connection [HikariProxyConnection@441638108 wrapping org.mariadb.jdbc.MariaDbConnection@159b2e33] will be managed by Spring
==>  Preparing: INSERT INTO article ( id,title,category_id,article_type,article_cover,is_appoint,article_status,publish_user_id,publish_time,read_counts,comment_counts,mongo_file_id,is_delete,create_time,update_time,content ) VALUES( ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,? ) 
==> Parameters: 240801FNS7M8G354(String), b10(String), 14(Integer), 2(Integer), (String), 1(Integer), 1(Integer), 240629F21AK1BHX4(String), 2024-08-04 00:00:00.0(Timestamp), 0(Integer), 0(Integer), null, 0(Integer), 2024-08-01 20:36:24.971(Timestamp), 2024-08-01 20:36:24.971(Timestamp), <p>b10</p>(String)
//★ <==    Updates: 1 ★
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472]
//★ 2天3小时23分钟 ★
//★ 延迟消息-定时发布文章:Thu Aug 01 20:36:24 CST 2024 ★
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472] from current transaction
==>  Preparing: UPDATE article SET article_status = ? WHERE ( ( id = ? ) ) 
==> Parameters: 2(Integer), 240801FNS7M8G354(String)
<==    Updates: 1

互联网框架演变【微服务块】

  • 架构演变
  • 微服务入门
  • SpringCloud各个组件学习
  • 改造项目服务化

注冊中心模型

  • Eureka
  • 可以把每個服務注入到eureka,更利於管理和維護,使得服務閒通信更方便

Lilei [上户口] → 派出所 ← [上户口] HanMeimei

构建Eureka注册服务【eureka】

springcloud-eureka  com/imooc/eureka/Application.java
package com.imooc.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, MongoAutoConfiguration.class})
@EnableEurekaServer // 开启注册中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
http://localhost:7000/hello  #运行接口
http://localhost:7000         #运行eureka
springcloud-eureka  com/imooc/eureka/controller/HelloController.java
package com.imooc.eureka.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        return GraceJSONResult.ok();
    }
}
springcloud-eureka  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-eureka</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!-- 其他必要的依赖 -->
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
springcloud-eureka  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-dev/springcloud-eureka"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/eureka.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
springcloud-eureka  application.yml
############################################################
#
# eureka 注册中心
# web访问端口号  约定:7000
#
############################################################
server:
  port: 7000
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:  # eureka 实例的hostname,也可以是自定义配置hostname
    hostname: eureka
  client:  # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
http://localhost:7000/
进入了Spring Eureka
Instances currently registered with Eureka

实现用户与文章的服务注册【eureka】

关于eureka客户端启动报错UnknownHostException详细解决方法_eureka unknownhostexception-CSDN博客

# 问题:
http://eureka:7000/eureka   找不到 eureka 的服务器 IP 地址

在cmd里面
C:\Users\Pluminary>ping eureka
Ping 请求找不到主机 eureka。请检查该名称,然后重试。

# 解答:
从你的描述来看,主机eureka无法解析,这是导致服务无法注册到Eureka Server的原因。你可以通过以下方法解决这个问题:

1. 更新 hosts 文件
在你的开发机器上更新 hosts 文件以手动解析 eureka 主机名。

Windows
打开记事本以管理员身份运行。

打开文件 C:\Windows\System32\drivers\etc\hosts。

添加以下行,将 <eureka服务器的IP地址> 替换为实际的IP地址:

plaintext
复制代码
<eureka服务器的IP地址> eureka
保存并关闭文件。

=================================================================
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com
springcloud-eureka  resources/application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # eureka 实例的hostname,可以是hostname,也可以自定义配置hostname
    hostname: eureka
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
service-user  resources/application.yml
上面的其余不变 底下添加eureka
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
      defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient // 开启eureka client 注册到server中
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
springcloud-eureka  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-eureka</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

    </dependencies>

</project>
springcloud-eureka  com/imooc/user/Application.java
package com.imooc.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, MongoAutoConfiguration.class})
@EnableEurekaServer // 开启注册中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
====================================================================
// 先启动这个服务 再启动user的服务
随后就能看见http://localhost:7000/
里面有一个注册的服务
/*  
   Instances currently registered with Eureka
   Application    AMIs    Availability Zones    Status
★ SERVICE-USER    n/a (1)    (1)    UP (1) - localhost:service-user:8003 
*/
service-article  resources/application.yml
# 定义freemarker生成的HTML
freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
      defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
service-article  com/imooc/article/Application.java 
//【此时再去启动这个article服务 会发现SERVICE-ARTICLE也成功的注册到Eureka中】
package com.imooc.article;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
/*
   Application    AMIs    Availability Zones    Status
★ SERVICE-ARTICLE    n/a (1)    (1)    UP (1) - localhost:service-article:8001
★ SERVICE-USER    n/a (1)    (1)    UP (1) - localhost:service-user:8003
*/

使用AppName优化服务间的通信【eureka】

实行动态化调用 地址拼接

AppName是eureka的ApplicationId = SERVICE-USER

慕课新闻·[文章article]自媒体接口api 如果页面没有信息那就是在Swagger2.java中代码的问题
Eureka

门户端文章业务的controller → /portal/article/detail → articleId=2006117B57WRZGHH

package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
    //    http://localhost:8088/doc.html            新路径

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
//                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
service-article  com/imooc/article/controller/ArticlePortalController.java
// 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String serviceId = "SERVICE-USER";
        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
        String userServerUrlExecute
                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }

动态构建eureka集群【eureka】保证高可用

创建一个新的module 其内容和 springcloud-eureka 里面的一样

SwitchHosts配置信息
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com

# SpringCloud
127.0.0.1 eureka
127.0.0.1 eureka-cluster-7001
127.0.0.1 eureka-cluster-7002
127.0.0.1 eureka-cluster-7003
springcloud-eureka-cluster  application.yml
############################################################
#
# eureka 集群的注册中心
# web访问端口号  约定:7001~7003
#
############################################################
server:
  port: 7001
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka-cluster

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-7001
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

####################################################################################
http://localhost:7001/

DS Replicas
eureka-cluster-7003
eureka-cluster-7002
####################################################################################
如果后面服务很多 100个 那是不是也要创建100个module呢?
并不是 因为每个都是一样的只是改一下application.yml的port端口号而已
所以我们要去把它设置为动态的端口

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

springcloud-eureka-cluster  application.yml【改后】
############################################################
#
# eureka 集群的注册中心
# web访问端口号  约定:7001~7003
#
############################################################
server:
  port: ${7001}
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka-cluster

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-${server.port}
    # 自定义端口号
  other-node-port2: ${p2:7002}
  other-node-port3: ${p3:7003}
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-${eureka.other-node-port2}:${eureka.other-node-port2}/eureka/,http://eureka-cluster-${eureka.other-node-port3}:${eureka.other-node-port3}/eureka/

####################################################################################
http://eureka-cluster-7001:7001/  #【可运行】

1一站式管理所有SpringBoot启动类,Services服务窗口 - 喵酱张-Eric - 博客园 (cnblogs.com)
IDEA 2021没有VM options_idea2021怎么找到“vm options”-CSDN博客

复制eureka-cluster-7001服务 变成eureka-cluster-7002 并且在Edit configuration的地方点击Modify options中的Add VM 输入代码:**-DPORT=7002 -DP2=7001 -DP3=7003**
同理弄一个eureka-cluster-7003 输入VM代码:**-DPORT=7003 -DP2=7001 -DP3=7002**
全部启动后都可以在浏览器正常运行 【集群】
http://eureka-cluster-7001:7001/ DS Replicas:eureka-cluster-7003 + eureka-cluster-7002
http://eureka-cluster-7002:7002/ DS Replicas:eureka-cluster-7003 + eureka-cluster-7001
http://eureka-cluster-7003:7003/ DS Replicas:eureka-cluster-7002 + eureka-cluster-7001
如果把里面的application.yml配置注释掉 就可以把自己注册到eureka中
client:
# *是否要把当前的eureka server注册到自己
* register-with-eureka: false
# *从注册中心获得检索服务实例,server没有必要,直接false即可
* fetch-registry: false
之后再重新启动eureka-cluster-7001~7003

eureka-cluster-7001:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

微服务注册到eureka集群【eureka】${port:8003}

service-user  application.yml
    # 注册中心的服务地址
    service-url:
  # defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/ 三个节点的注册
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
service-article  application.yml
    # 注册中心的服务地址
    service-url:
  # defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

慕课新闻·自媒体接口:8001_api
门户端文章业务的controller → get:/portal/article/detail → articleId:2006117B57WRZGHH

Eureka:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (1) (1) UP (1) - localhost:service-user:8003
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

构建微服务集集群【eureka】

复制service-user:8003服务 变成service-user:8013 并且在Edit configuration的地方点击Modify options中的Add VM 输入代码:**–DPORT=8013**

service-user  application-dev.yml
server:
  port: ${port:8003}

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

Eureka:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (2) (2) UP (2) - localhost:service-user:8003 , localhost:service-user:8013
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

实现轮训负载均衡【eureka】

imooc-news-dev-service-user  application-dev.yml
server:
  port: ${port:8003}

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com
service-user  com/imooc/user/controller/UserController.java
//每次调用的时候 都会输出其Port
 @Value("${server.port}")
    private String myPort;

    @Override
    public GraceJSONResult queryByIds(String userIds) {
        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-article  com/imooc/article/controller/ArticlePortalController.java
    // 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String serviceId = "SERVICE-USER";
//        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
//        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
        String userServerUrlExecute
                //因为seviceId里面的SERVICE—USER就存在着userService.getHost()和.getPort()
                = "http://"+ serviceId + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        // 为restTemplate增加一个负载均衡 @CloudConfig 
                // public RestTemplate restTemplate()
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }
service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }
    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

慕课新闻·自媒体接口:8001-api
门户端文章业务的controller → articleId:2006117B57WRZGHH

自我保护功能【eureka】

springcloud-eureka-cluster  application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-${server.port}
    # 自定义端口号
  other-node-port2: ${p2:7002}
  other-node-port3: ${p3:7003}
  client:
    # 是否要把当前的eureka server注册到自己
      # register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
      # fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-${eureka.other-node-port2}:${eureka.other-node-port2}/eureka/,http://eureka-cluster-${eureka.other-node-port3}:${eureka.other-node-port3}/eureka/
  server:
    enable-self-preservation: false # 关闭eureka的自我保护功能
    eviction-interval-timer-in-ms: 5000 # 清理无效节点的时间,可以缩短为5s 默认60s
springcloud-eureka application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # eureka 实例的hostname,可以是hostname,也可以自定义配置hostname
    hostname: eureka
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false # 关闭eureka的自我保护功能
    eviction-interval-timer-in-ms: 5000 # 清理无效节点的时间,可以缩短为5s 默认60s
service-user  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/ 三个节点的注册
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

Eureka:7001
先把所有服务全启动
eureka:7000
eureka-cluster-7001
eureka-cluster-7002
eureka-cluster-7003
service-article:8001
service-user:8003
service-user:8013
然后去刷新

Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (2) (2) UP (2) - localhost:service-user:8003 , localhost:service-user:8013
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

随后只留下eureka-cluster-7001其他全部Stop
再次刷新

Instances currently registered with Eureka
Application AMIs Availability Zones Status
SPRINGCLOUD-EUREKA-CLUSTER n/a (1) (1) UP (1) - localhost:springcloud-eureka-cluster:7001

负载均衡工具

  • Ribbon[本地] = RestTemplate + @LoadBalanced
  • 服务间通信的负载均衡工具,提供完善的超时重试机制

service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡 默认的负载均衡算法:枚举
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

实现多种负载均衡算法【ribbon】

自定义规则

慕课新闻·自媒体接口api

service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡 默认的负载均衡算法:枚举
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}
service-api  com/rule/MyRule.java
package com.rule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 官方定义了规则不要被 @ComponentScan( 扫描到
@Configuration
public class MyRule {
    @Bean
    public IRule iRule(){// 随机的负载均衡策略
        return new RandomRule();
        // 在调用方article的启动类开启注解 @RibbonClient
    }
}
service-article  com/imooc/article/Application.java
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
运用配置文件进行配置

慕课新闻·自媒体接口api 发送11次请求 门户端文章业务→articleId:2006117B57WRZGHH
service-user:8003请求到myPort=8003 8次
service-user:8013请求到myPort=8013 3次

service-article  resources/application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
service-article  com/imooc/article/Application.java
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

重试机制【ribbon】

节点有可能因为网络问题访问不到 而为了不让其返回错误 需要重试机制
sevice-api  pom.xml
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule


ribbon:
  ConnectTimeout: 5000          # 创建连接的超时时间,单位:ms
  ReadTimeout: 5000             # 在连接创建好以后,调用接口的超时时间,单位:ms
  MaxAutoRetries: 1             # 最大重试次数
  MaxAutoRetriesNextServer: 2   # 切换到下个微服务实例的重试次数
  # 当请求到某个微服务5s,超时后会进行重试,先重试连接自己当前的这个实例
  # 如果当前重试失败1次,则会切换到访问集群中的下一个微服务实例,切换最大为2次

logging:
  level:
#    com.imooc.api.controller.user.UserControllerApi: debug
    root: debug

慕课新闻·自媒体接口-8001api articleId:2006117B57WRZGHH
先把所有服务全部启动 然后把service-user:8013 的服务Stop
再去api接口发送请求 查看servcice-article:8001的Console输出日志
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=0
14:10.774 [http-nio-8001-exec-1] DEBUG o.s.web.client.RestTemplate - Response 200 OK
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=1
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=2
14:10.774 [http-nio-8001-exec-1] DEBUG o.s.web.client.RestTemplate - Response 200 OK

简化服务调用【feign】以Api作为接口,面向接口的编程风格

声明式HTTP工具
  • Feign
  • 声明式的http工具,用于简化服务调用
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
service-article  com/imooc/article/controller/ArticlePortalController.java
    // 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private UserControllerApi userControllerApi;
    //面向接口 UserControllerApi
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
//        String serviceId = "SERVICE-USER";
//        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
//        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
//        String userServerUrlExecute
                //因为seviceId里面的SERVICE—USER就存在着userService.getHost()和.getPort()
//                = "http://"+ serviceId + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        GraceJSONResult bodyResult = userControllerApi.queryByIds(JsonUtils.objectToJson(idSet));
//        String userServerUrlExecute
//                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        // 为restTemplate增加一个负载均衡@CloudConfig public RestTemplate restTemplate()
//        ResponseEntity<GraceJSONResult> responseEntity
//                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
//        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }
service-article  com/imooc/article/Application.java //【EnableFeignClients】
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
@EnableFeignClients({"com.imooc"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-api  com/imooc/api/config/MyServiceList.java
package com.imooc.api.config;

public class MyServiceList {
    public static final String SERVICE_USER = "service-user";
}
service-article  com/imooc/api/controller/user/UserControllerApi.java【@FeignClient】
package com.imooc.api.controller.user;

import com.imooc.api.config.MyServiceList;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
@FeignClient(value = MyServiceList.SERVICE_USER) //作为客户端直接调用
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          @RequestParam BindingResult result); //RequestParam  BindingResult result 加了肯定在前端不可用 对Feign而言不能存在两个对象不然会认为有两个model

    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByIds")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}

统一检验处理【feign】

把所有的BindingResult都可以采用全局调用的方法去调用
慕课新闻·自媒体接口8003api → 用户信息相关 → 修改/完善用户信息 Post /user/updateUserInfo

dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import javax.naming.Binding;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice
public class GraceExceptionHandler {
 /* @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseBody
    public GraceJSONResult returnMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
        return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_MAX_SIZE_ERROR);
    }*/

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody //该异常是基于所有的vo验证
    public GraceJSONResult returnException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        Map<String, String> map = getErrors(result);
        return GraceJSONResult.errorMap(map);
    }

    public Map<String, String> getErrors(BindingResult result) {
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList) {
            // 发送验证错误的时候所对应的某个属性
            String field = error.getField();
            // 验证的错误消息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
//    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
//    @PostMapping("/updateUserInfo")
//    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
//                                          @RequestParam BindingResult result);
    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO);
service-user  com/imooc/user/controller/UserController.java
    @Override
  public GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO){
    //, BindingResult result) {
//        // 0.校验BO
//        if (result.hasErrors()){
//            Map<String, String> map = getErrors(result);
//            return GraceJSONResult.errorMap(map);
//        }
        // 1.执行更新操作
        userService.updateUserInfo(updateUserInfoBO);
        return GraceJSONResult.ok();
        //调用UserService把独有信息传入
    }

开启日志调式【feign】基于http的调用

service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule


ribbon:
  ConnectTimeout: 5000          # 创建连接的超时时间,单位:ms
  ReadTimeout: 5000             # 在连接创建好以后,调用接口的超时时间,单位:ms
  MaxAutoRetries: 1             # 最大重试次数
  MaxAutoRetriesNextServer: 2   # 切换到下个微服务实例的重试次数
  # 当请求到某个微服务5s,超时后会进行重试,先重试连接自己当前的这个实例
  # 如果当前重试失败1次,则会切换到访问集群中的下一个微服务实例,切换最大为2次

logging:
  level:
    com.imooc.api.controller.user.UserControllerApi: debug
#    root: debug 日志打印级别

# 配置feign
feign:
  client:
    config:
      # 配置服务提供方的名称
      service-user:
        logger-level: full

重启所有服务 调用慕课新闻·自媒体接口8001api门户端→文章详情 articleId:2006117B57WRZGHH
09:19.009 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] <— HTTP/1.1 200 (590ms)
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] connection: keep-alive
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] content-type: application/json
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] date: Mon, 05 Aug 2024 09:09:19 GMT
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] keep-alive: timeout=60
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] transfer-encoding: chunked
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Access-Control-Request-Headers
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Access-Control-Request-Method
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Origin
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds]
09:19.013 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] {“status”:200,”msg”:”操作成功!”,”success”:true,”data”:[{“id”:”200628AFYM7AGWPH”,”nickname”:”我是慕课网”,”face”:”https://imooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/200628AFYM7AGWPH/2007088XH2WT7GXP.png","activeStatus":1,"myFollowCounts":null,"myFansCounts":null}]}
09:19.013 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] <— END HTTP (286-byte body)
09:19.518 [PollingServerListUpdater-0] INFO c.n.config.ChainedDynamicProperty - Flipping property: service-user.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
09:21.013 [scheduling-1] INFO c.imooc.api.aspect.ServiceLogAspect - 开始执行 class com.imooc.article.service.impl.ArticleServiceImpl.updateAppointToPublish

阐述断路器及概念【hystrix】

断路器
  • Hystrix
  • 提供容错机制,避免微服务系统雪崩
服务熔断与降级

模拟服务故障【hystrix】

慕课新闻·自媒体接口8001api 同上
会报Timeout超时的异常

service-user  com/imooc/user/controller/UserController.java
   @Value("${server.port}")
    private String myPort;

    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

服务提供者熔断【hystrix】

service-user  com/imooc/user/controller/UserController.java
@Value("${server.port}")
    private String myPort;
    // 添加熔断机制 一旦熔断会有替补方法[降级的方法]
    @HystrixCommand(fallbackMethod = "queryByIdsFallback")
    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }


    public GraceJSONResult queryByIdsFallback(String userIds) {
        System.out.println("进入降级方法:queryByIdsFallback");

        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            // 手动构建空对象,详情页所展示的用户信息可有可无 返回空对象
            AppUserVO userVO = new AppUserVO();
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient   // 开启eureka client 注册到server中
@EnableCircuitBreaker // 开启hystrix的熔断机制
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
===============================================================
进入降级方法
service-user  application.yml
#  配置hystrix
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000   # 设置hystrix超时时间,超过2秒触发降级

全局降级【hystrix】

只需要return一个错误就行了没必要100个方法写100个降级
{

​ status: 555,
​ msg: “”全局降级:系统繁忙,请稍后再试!””
​ success: false,
​ data: null

}

service-user  com/imooc/user/controller/UserController.java
@RestController
@DefaultProperties(defaultFallback = "defaultFallback")
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    // 其他方法一旦发现异常就会进入这个方法里面 全局唯一 其他的降级方法要注释
    public GraceJSONResult defaultFallback(){
        return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_GLOBAL);
    }
......
}
// 改动是为了不报错空指针异常 因为已经变成了全局降级  降级的错误信息要调整
    @Autowired
    private UserControllerApi userControllerApi;
    //面向接口 UserControllerApi
    // 发起远程调用,获得用户的基本信息
   /* private List<AppUserVO> getPublisherList(Set idSet) {
GraceJSONResult bodyResult = userControllerApi.queryByIds(JsonUtils.objectToJson(idSet)); 
List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }*/ else {
            publisherList = new ArrayList<>();
        }
        return publisherList;
    }
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
 // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, ""),
    SYSTEM_ERROR_GLOBAL(558, false, "全局降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!");

服务调用者降级【hystrix】

service-article  application.yml
# 配置feign
feign:
  client:
    config:
      # 配置服务提供方的名称
      service-user:
        logger-level: full

  hystrix:  #打开feign客户端的内置hystrix
    enabled: true
service-article  com/imooc/article/Application.java //【增加一个@EnableHystrix】
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
@EnableFeignClients({"com.imooc"})
@EnableHystrix
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
====================================================================================
service-article:8001  进入客户端(服务调用者)的降级方法
service-api  com/imooc/api/controller/user/UserControllerApi.java//【@FeignClient增加fallbackFactory】
package com.imooc.api.controller.user;

import com.imooc.api.config.MyServiceList;
import com.imooc.api.controller.user.fallbacks.UserControllerFactoryFallback;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user") //fallbackFactory所有方法的降级
@FeignClient(value = MyServiceList.SERVICE_USER, fallbackFactory = UserControllerFactoryFallback.class ) //作为客户端直接调用
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

//    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
//    @PostMapping("/updateUserInfo")
//    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
//                                          @RequestParam BindingResult result);
    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO);


    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByIds")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}
service-api  com/imooc/api/controller/user/fallbacks/UserControllerFactoryFallback.java
package com.imooc.api.controller.user.fallbacks;

import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.pojo.vo.AppUserVO;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

@Component //这个类让容器加载
public class UserControllerFactoryFallback implements FallbackFactory<UserControllerApi> {
    @Override
    public UserControllerApi create(Throwable throwable) {
 // 重写的过程就是降级的过程
        return new UserControllerApi() {
            //SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!")
            @Override
            public GraceJSONResult getUserInfo(String userId) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult getAccountInfo(String userId) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult queryByIds(String userIds) {
                System.out.println("进入客户端(服务调用者)的降级方法");
                List<AppUserVO> publisherList = new ArrayList<>();
                return GraceJSONResult.ok(publisherList);
            }
        };
    }
}

自动触发熔断隔离与恢复【hystrix】

service-user  application.yml #【配置熔断器】
#  配置hystrix
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000   # 设置hystrix超时时间,超过2秒触发降级
      circuitBreaker:   # 配置断路器
        enabled: true
        requestVolumeThreshold: 10    # 触发熔断最小请求次数,默认:20
        sleepWindowInMilliseconds: 15000    # 熔断后过几秒后尝试半开状态(请求重试),默认:5s
        errorThresholdPercentage: 50  # 触发熔断的失败率(异常率/阈值),默认:50
service-user  com/imooc/user/controller/UserController.java //[FIXME:]
    @Value("${server.port}")
    private String myPort;
    // 添加熔断机制 一旦熔断会有替补方法[降级的方法]
    @HystrixCommand//(fallbackMethod = "queryByIdsFallback")
    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        // FIXME: 仅用于dev测试,硬编码动态判断来抛出异常
        if (userIdList.size() > 1){
            System.out.println("出现异常~~");
            throw new RuntimeException("出现异常~~");
        }

        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }

微服务网关【zuul】维护微服务的ip地址

服务网关
  • Zuul (祖尔)
  • 微服务的网关,可以实现动态路由、过滤器等功能

构建网关微服务【zuul】

springcloud-zuul-server  com/imooc/zuul/Application.java
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
//@EnableZuulServer
@EnableZuulProxy      // @EnableZuulProxy是@EnableZuulServer的一个增强升级版,当zuul和eureka、ribbon等组件共同使用,则使用增强版即可
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
springcloud-zuul-server  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/springcloud-zuul"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/zuul.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
springcloud-zuul-server  pom.xml 【exclusions是重点】
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-zuul-server</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version> <!--排除包-->
                <exclusions>
                    <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                    </exclusion>
                </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--<dependency>-->
        <!--<groupId>org.springframework.cloud</groupId>-->
        <!--<artifactId>spring-cloud-starter-bus-amqp</artifactId>-->
        <!--</dependency>-->

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

    </dependencies>

</project>
springcloud-zuul-server  application.yml 
############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1
zuul-server  com/imooc/zuul/controller/HelloController.java
package com.imooc.zuul.controller;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
        return GraceJSONResult.ok();
    }
}

配置路由【zuul】

zuul-server  application.yml
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
      path: /service-article/**       # 请求路径(前缀)
      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
localhost:8001/portal/article/detail?articleId=2006117B57WRZGHH
直接可以访问到详情数据
微服务网关→ 7070
// service-article: /service-article/** # 请求路径(前缀**)
# localhost:7070/service-article/portal/detail?articleId=2006117B57WRZGHH

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
      path: /service-article/**       # 请求路径(前缀)
      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀
# localhost:7070/api/service-article/portal/detail?articleId=2006117B57WRZGHH

配置微服务实例的路由【zuul】

在路由规则里面的  url: http://192.168.1.2:8001  很容易发生变化
所以直接去请求eureka的SERVICE-USER/ARTICLE
springcloud-zuul-server  pom.xml 【取消exclusions注释 包含eureka client】
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version> <!--排除包-->
          </dependency>
zuul-server  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
springcloud-zuul-server  com/imooc/zuul/Application.java 【打开@EnableEurekaClient】
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
//@EnableZuulServer
@EnableZuulProxy      // @EnableZuulProxy是@EnableZuulServer的一个增强升级版,当zuul和eureka、ribbon等组件共同使用,则使用增强版即可
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
zuul-server  application.yml 【实现service-id进行请求转发 ip发生变化没有关系】
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
      service-article:                  # 配置微服务的路由id,微服务的实例id
      path: /service-article/**       # 请求路径(前缀)
      service-id: service-article     # 请求转发的微服务实例id
#     url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 简化版本 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

过滤器【zuul】网端ip黑名单拦截

zuul-server  com/imooc/zuul/filters/MyFilter.java
package com.imooc.zuul.filters;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

/**
 * 构建zuul的自定义过滤器
 */
@Component
public class MyFilter extends ZuulFilter {

    /**
     * 定义过滤器的类型
     *      pre:    在请求被路由之前执行
     *      route:  在路由请求的时候执行
     *      post:   请求路由以后执行
     *      error:  处理请求时发生错误的时候执行
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器执行的顺序,配置多个有顺序的过滤
     * 执行顺序从小到大
     * @return
     */
    @Override
    public int filterOrder() {
        return 1;
    }

    /**
     * 是否开启过滤器
     *      true:开启
     *      false:禁用
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的业务实现
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {

        System.out.println("display pre zuul filter...");

        return null;    // 没有意义可以不用管。
    }
}

localhost:7070/api/service-article/portal/detail?articleId=2006117B57WRZGHH
刷新成功后 在zuul-7070服务console会有一行
display pre zuul filter… [启动成功]

限制ip黑名单的频繁请求【zuul】

zuul-server  application.yml 【增加ip请求限制的参数配置】
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
      service-article:                  # 配置微服务的路由id,微服务的实例id
      path: /service-article/**       # 请求路径(前缀)
      service-id: service-article     # 请求转发的微服务实例id
#     url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 简化版本 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒
zuul-server  com/imooc/zuul/filters/BlackIPFilter.java
package com.imooc.zuul.filters;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
/* application.yml
*  blackIp:
   continueCounts: ${counts:10}    # ip连续请求的次数
   timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
   limitTimes: ${times:15}         # 限制的事件,单位:秒
* */
@Component
@RefreshScope
public class BlackIPFilter extends ZuulFilter {

    @Value("${blackIp.continueCounts}")
    public Integer continueCounts;
    @Value("${blackIp.timeInterval}")
    public Integer timeInterval;
    @Value("${blackIp.limitTimes}")
    public Integer limitTimes;

    @Autowired
    private RedisOperator redis;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        System.out.println("执行【ip黑名单】过滤器...");

        System.out.println("continueCounts: " + continueCounts);
        System.out.println("timeInterval: " + timeInterval);
        System.out.println("limitTimes: " + limitTimes);


        // 获得上下文对象
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        // 获得ip
        String ip = IPUtil.getRequestIp(request);

        /**
         * 需求:
         *  判断ip在10秒内的请求次数是否超过10次
         *  如果超过,则限制这个ip访问15秒,15秒以后再放行
         */

        final String ipRedisKey = "zuul-ip:" + ip;
        final String ipRedisLimitKey = "zuul-ip-limit:" + ip;

        // 获得当前ip这个key的剩余时间
        long limitLeftTime = redis.ttl(ipRedisLimitKey);
        // 如果当前限制ip的key还存在剩余时间,说明这个ip不能访问,继续等待
        if (limitLeftTime > 0) {
            stopRequest(context);
            return null;
        }

        // 在redis中累加ip的请求访问次数
        long requestCounts = redis.increment(ipRedisKey, 1);
        // 从0开始计算请求次数,初期访问为1,则设置过期时间,也就是连续请求的间隔时间
        if (requestCounts == 1) {
            redis.expire(ipRedisKey, timeInterval);
        }

        // 如果还能取得请求次数,说明用户连续请求的次数落在10秒内
        // 一旦请求次数超过了连续访问的次数,则需要限制这个ip的访问
        if (requestCounts > continueCounts) {
            // 限制ip的访问时间
            redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);
            stopRequest(context);
        }

        return null;
    }

    private void stopRequest(RequestContext context) {
        // 停止zuul继续向下路由,禁止请求通信
        context.setSendZuulResponse(false);
        context.setResponseStatusCode(200);
        String result = JsonUtils.objectToJson(
                GraceJSONResult.errorCustom(
                        ResponseStatusEnum.SYSTEM_ERROR_ZUUL));
        context.setResponseBody(result);
        context.getResponse().setCharacterEncoding("utf-8");
        context.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
    }
}

=============================================================================
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!");
zuul-server  application.yml 【把redis增加进来】
############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀



# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒

此时redis中会出现
zuul-ip(1) → zuul-ip → Value:1
zuul-ip-limit(1) → zuul-ip-limit: 本机地址
在zuul-7070服务里的Console 显示:
display pre zuul filter …
执行【ip黑名单】过滤器…

分布式配置中心【config】

  • SpringCloud Config
  • 为所有服务提供统一的配置管理服务微服务配置一下子全部生效
  • 包含配置服务端与配置客户端
配置中心的功能
  • 统一管理配置
  • 管理不同环境下的配置
  • 动态调整配置

搭配配置中心【config】

leechenxiang/imooc-news-config (github.com)

zuul-dev.yml
blackIp:
  continueCounts: 10
  timeInterval: 10
  limitTimes: 15

zuul-prod.yml
blackIp:
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315
springcloud-config  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-config</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
    </dependencies>
</project>
springcloud-config  com/imooc/config/Application.java
package com.imooc.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
==========================================================================
config-7080
springcloud-config  application.yml
############################################################
#
# 配置服务Config
# web访问端口号  约定:7080
#
############################################################
server:
  port: 7080
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-config
  
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/


# 配置动态刷新git配置的路径终端请求地址
management:
  endpoints:
    web:
      exposure:
        include: "*"
springcloud-config  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/springcloud-config"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/config.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
springcloud-config  com/imooc/config/controller/HelloController.java
package com.imooc.config.controller;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public Object hello() {
        return GraceJSONResult.ok();
    }
}

配置中心实现git配置读取【config】

springcloud-config  pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-config</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>

</project>
springcloud-config  application.yml
#增加  cloud:config:server:git: uri
############################################################
#
# 配置服务Config
# web访问端口号  约定:7080
#
############################################################
server:
  port: 7080
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-config
  cloud:
    config:
      server:
        git:
          uri: https://github.com/leechenxiang/imooc-news-config.git
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
springcloud-config  com/imooc/config/Application.java
package com.imooc.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
@EnableConfigServer //开启这个配置中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

leechenxiang/imooc-news-config (github.com)

localhost:7080/zuul-prod.yml 这里直接引用了github上面的yml
localhost:7080/master/zuul-prod.yml

zuul-dev.yml
blackIp:
  continueCounts: 10
  timeInterval: 10
  limitTimes: 15

zuul-prod.yml
blackIp:
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315

配置客户端拉取配置

zuul-server  pom.xml
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
zuul-server  resources/bootstrap.yml
############################################################
# 系统全局加载文件 先加载这个文件
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server
  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  cloud:
    config:
      label: master # github上的分支
      name: zuul # 定义的服务
      profile: prod  # 所加载的环境变量
#      uri: http://192.168.1.2:7080
      discovery:
        enabled: true
        service-id: springcloud-config


############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/**
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址
  prefix: /api                        # 请求前缀
启动zuul-7070  //客户端连接config(server)并且动态获得了github的数据
打印控制台Console
display pre zuul filter...
执行【ip黑名单】过滤器...
// zuul-prod.yml
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315

动态刷新git配置

// zuul-prod.yml 动态修改数值
  continueCounts: 35
  timeInterval: 305
  limitTimes: 65
在浏览器会显示更新后的数值 但是在console打印台不会显示改后的 只会显示之前的
需要重启服务才可以达到修改后的效果 显示修改的数值
zuul-server  pom.xml
添加健康检测的配置
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
zuul-server  com/imooc/zuul/filters/BlackIPFilter.java
//【开启刷新 @RefreshScope 不能全自动 需要触碰某些请求 在yaml配置动态刷新的地址】
package com.imooc.zuul.filters;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
/* application.yml
*  blackIp:
   continueCounts: ${counts:10}    # ip连续请求的次数
   timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
   limitTimes: ${times:15}         # 限制的事件,单位:秒
* */
@Component
@RefreshScope
public class BlackIPFilter extends ZuulFilter {

    @Value("${blackIp.continueCounts}")
    public Integer continueCounts;
    @Value("${blackIp.timeInterval}")
    public Integer timeInterval;
    @Value("${blackIp.limitTimes}")
    public Integer limitTimes;

    @Autowired
    private RedisOperator redis;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }
}
zuul-server  bootstrap.yml  +  springcloud-config  application.yaml
# 配置动态刷新git配置的路径终端请求地址 只需要通过URL请求不用重启就可以自动刷新 后期要通过脚本运行才可【通过请求处理 postman → POST → https://localhost:7070/actuator/refresh】
management:
  endpoints:
    web:
      exposure:
        include: refresh
# 此时回到console就可以有更新后的数据显示了

消息总线概述【bus】[RabbitMQ]

Config遗留问题
  • 手动刷新与业务耦合 [也可能在文章/user模块发生]
  • Config配置中心的刷新去解决动态刷新
  • N个微服务端需要N次手动书信
消息总线(要和config与微服务端进行配置)
  • SpringCloud Bus
  • 为SpringCloud Config提供增益buff
  • 可以实现配置自动刷新[1k个 1w个]

配置实现消息统一发送【bus】

config把消息推給zull-server

springcloud-config  pom.xml    +    zuul-server  pom.xml
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
zuul-server  resources/bootstrap.yaml  +  springcloud-config  application.yaml
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

实现刷新server端达到所有的刷新配置
【通过请求处理 postman → POST → https://localhost:7080/actuator/bus-refresh】
若想精确刷新某个服务 需要拼接
postman → POST → https://localhost:7080/actuator/bus-refresh/{微服务的实例id}:{port}
微服务实例id是yaml配置项目信息 → spring.application.name:springcloud-zuul-server
https://localhost:7080/actuator/bus-refresh/springcloud-zuul-server:7070 【目标微服务地址实现精确打击】

消息驱动概述【stream】

消息驱动
  • SpringCloud Stream
  • 统一封装消息的服务框架
  • RabbitMQ,RocketMQ,Kafka,ActiveMQ,ZeroMQ,…
Stream消息模型

实现消费者与生产者【stream】

将zuul-server的resources/application.yml 恢复到原来配置

############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀



# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒
service-article  pom.xml  +  service-user  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            <version>4.1.1</version>
        </dependency>
service-article  application.yaml      +      service-user  application.yaml
    cloud:
    stream:
      bindings:                    # 绑定通道和交换机
        myOutput:                   # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput:                    # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
service-article  com/imooc/article/stream/MyStreamChannel.java
package com.imooc.article.stream;

import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;

/**
 * 声明构建通道channel
 */
@Component
public interface MyStreamChannel {

    String OUTPUT = "myOutput";
    String INPUT = "myInput";

    @Output(MyStreamChannel.OUTPUT)
    MessageChannel output();

    @Input(MyStreamChannel.INPUT)
    SubscribableChannel input();  //订阅能力的通道
}
service-article  com/imooc/article/stream/MyStreamChannel.java
package com.imooc.article.stream;


import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;

/**
 * 声明构建通道channel
 */
@Component
public interface MyStreamChannel {
    String OUTPUT = "myOutput";
    String INPUT = "myInput";

    @Output(MyStreamChannel.OUTPUT)
    MessageChannel output();

    @Input(MyStreamChannel.INPUT)
    SubscribableChannel input();
}
service-article  com/imooc/article/stream/StreamService.java
package com.imooc.article.stream;

public interface StreamService {
    public void sendMsg();
    public void eat(String dumpling);
}
service-article  com/imooc/article/stream/StreamServiceImpl.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 开启绑定器
 * 绑定通道channel
 */
@Component
@EnableBinding(MyStreamChannel.class)
public class StreamServiceImpl implements StreamService {

    @Autowired
    private MyStreamChannel myStreamChannel;

    @Override
    public void sendMsg() {
        AppUser user = new AppUser();
        user.setId("10101");
        user.setNickname("imooc");

        // 消息通过绑定器发送给mq
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(user).build());
    }

    @Override
    public void eat(String dumpling) {
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(dumpling).build());
    }
}
service-article  com/imooc/article/stream/MyStreamConsumer.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;

/**
 * 构建消费端
 */
@Component
@EnableBinding(MyStreamChannel.class) //开启通道绑定
public class MyStreamConsumer {

    /**
     * 监听并且实现消息的消费和相关业务处理
     */
    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(AppUser user) {
        System.out.println(user.toString());
    }

    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(String dumpling) {
        System.out.println(dumpling);
    }
}
service-article  com/imooc/article/controller/HelloController.java

@RestController
@RequestMapping("producer")
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StreamService streamService;

    @GetMapping("/stream")
    public Object stream() {
        streamService.sendMsg();

        for (int i = 0 ; i < 10 ; i ++ ) {
            streamService.eat("我吃了第" + (i+1) + "只饺子~");
        }

        return "ok~~!!!";
    }

消息分组与持久化【stream】

避免重复消费 分组group 组内消费者不会重复消费

Stream消息分组
service-article  application.yml  
  cloud:
    stream:
      bindings:                    # 绑定通道和交换机
        myOutput:                   # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput:                    # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
          group: boys

------------------------------------------------------------------------------

service-user  application.yml
  cloud:
    stream:
      bindings: # 绑定通道和交换机
        myOutput: # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput: # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
          group: girls
service-article  com/imooc/article/stream/StreamService.java
package com.imooc.article.stream;

public interface StreamService {
  //public void sendMsg();
    public void eat(String dumpling);
}
service-article  com/imooc/article/stream/StreamServiceImpl.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 开启绑定器
 * 绑定通道channel
 */
@Component
@EnableBinding(MyStreamChannel.class)
public class StreamServiceImpl implements StreamService {

    @Autowired
    private MyStreamChannel myStreamChannel;

    @Override
 /* public void sendMsg() {
        AppUser user = new AppUser();
        user.setId("10101");
        user.setNickname("imooc");

        // 消息通过绑定器发送给mq
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(user).build());
    } */

    @Override
    public void eat(String dumpling) {
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(dumpling).build());
    }
}
service-article  com/imooc/article/stream/MyStreamConsumer.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;

/**
 * 构建消费端
 */
@Component
@EnableBinding(MyStreamChannel.class) //开启通道绑定
public class MyStreamConsumer {

    /**
     * 监听并且实现消息的消费和相关业务处理
     */
 /* @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(AppUser user) {
        System.out.println(user.toString());
    } */

    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(String dumpling) {
        System.out.println(dumpling);
    }
}
service-article  com/imooc/article/controller/HelloController.java

@RestController
@RequestMapping("producer")
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StreamService streamService;

    @GetMapping("/stream")
    public Object stream() {
     // streamService.sendMsg();

        for (int i = 0 ; i < 10 ; i ++ ) {
            streamService.eat("我吃了第" + (i+1) + "只饺子~");
        }

        return "ok~~!!!";
    }

消息不会被重复消费
service-article:8001 我吃了1-10只饺子~ 【消费的饺子总数一共10次】
service-user:8003(女生) 我吃了1 3 5 7 9只饺子~ 【饺子随机】
service-user:8013(男生) 我吃了2 4 6 8 10只饺子~ 【饺子随机】

如果把service-user:8003(女生) service-user:8013(男生) 服务stop 用户微服务无法接收任何消息
但是我们定义了group → 消息是可以持久化的 当重启用户微服务之后 就会打印出刚刚已经吃的饺子了
服务器宕机 = 吃饺子中途去上厕所 回来后仍然还能吃到饺子

链路追踪概述与zipkin【sleuth】组件

链路追踪
  • Sleuth
  • 贯穿整个微服务系统中,追踪一个请求的过程
  • zipkin 可视化控制面板

下载zipkin-server-2.12.6-exec.jar
CMD → C:\Users\Pluminary>java -jar /Users/Pluminary/Desktop/zipkin-server-2.12.6-exec.jar
https://localhost:9411/zipkin/

整合zipkin【sleuth】项目入口是网关

zuul-server  pom.xml  +  service-article  pom.xml  +  service-user  pom.xml
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
zuul-server与service-article与service-user  application.yml
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

https://localhost:9411/zipkin/

SpringCloud章节总结

  • eureka 注册中心
  • ribbon 负载均衡
  • feign 声明式客户端
  • hystrix 熔断降级组件
  • zuul 网关
  • config 配置中心
  • bus 消息总线
  • stream 消息驱动
  • zipkin + sleuth 链路追踪
阅读全文

Zookeeper+Dubbo应用与面试

2024/5/4

Zookeeper+Dubbo与面试周介绍

  • Zookeeper的下载、配置与运行
  • 数据结构node与常用命令
  • Watcher机制和权限cal介绍
  • 使用Zookeeper的Java原生客户端和Curator进行开发
  • RPC调用,在Dubbo架构下各服务的关系
  • 整合Dubbo和Zookeeper
  • 完成Dubbo开发案例
  • 线程进阶面试
  • 分布式、微服务面试题
  • Spring Cloud、Zookeeper的理解

Zookeeper

  • 理解Zookeeper
  • 安装、配置
  • 节点znode
  • 常用命令
  • Watcher机制
  • ACL权限控制
  • 代码实操

理解Zookeeper

  • 5大特点
  • 集群架构
  • Zookeeper和CAP关系
  • Zookeeper作用

为什么需要Zookeeper

  • 用起来像单机但是又比单机更可靠

  • leader在团队里的协调作用

  • 内存、单机

  • 集群、可靠

  • 当信息还没同步完成时,不对外提供服务

  • 同步的时间压缩的更短

Zookeeper诞生历史

雅虎研究室

  • 无单点问题的分布式协调架构,精力集中在处理业务逻辑
  • 内部很多项目都是使用动物的名字来命名
  • 大型动物园

Zookeeper是什么 [底层是Java]

  • Zookeeper是开源的高性能的分布式应用协调系统,一个高性能的分布式数据一致性解决方案

5大特点

  • 顺序一致性
  • 原子性 [全部成功或者全部不成功]
  • 单一视图 [无论连接哪个 都是一致的信息]
  • 可靠性
  • 及时性 [一定时间内能从服务器读到状态]
架构图、集群、工作过程

Zookeeper和CAP的关系

  • CP:一致性+分区容错性
  • 得到一致的数据结果,同时系统对网络具备容错性
  • 但是它不能保证每次服务请求的可用性
作用
  • 分布式服务注册与订阅
  • 统一配置文件
  • 生成分布式唯一ID [/order-0000001、/order-0000002]
  • Master节点选举

针对不能同时进行写数据,保证互斥同步 → 分布式锁

Zookeeper的安装、配置

  • 寻找教辅里的apache-zookeeper-3.6.0-bin.tar
  • 解压压缩包:tar zxvf apache-zookeeper-3.6.0-bin.tar.gz
  • 进入压缩包:cd apache-zookeeper-3.6.0-bin
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls
bin  conf  docs  lib  LICENSE.txt  NOTICE.txt  README.md README_packaging.md
  • 配置[进入文件]:cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls conf/
configuration.xsl  log4j.properties  zoo  zoo.cfg  zoo_sample.cfg
  • 打开文件, 进行修改:vi conf/zoo.cfg
找到 dataDir=/tmp/zookeeper
修改成 dataDir=/tmp/lib/zookeeper 以免被自动清除
  • 启动:./bin/zkServer.sh start
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 1877.
======================================================================
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... ^[[ASTARTED
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh status
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: standalone
======================================================================
  • 停止:./bin/zkServer.sh stop
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh stop
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED

znode节点 [基本数据模型]

  • 树结构

节点性质
  • 树形结构,也可以理解为linux的文件目录
  • 每一个节点都是znode,里面可以包含数据,也可以有子节点
  • 点分为永久节点临时节点(与客户端绑定) [session失效,也就是客户端断开过,临时节点消失]
  • 每个znode都有版本号,每当数据变化,版本号都会累加(乐观锁)
  • 删除或修改节点,版本号不匹配的话(版本号已超时), 会报错)
  • 每个节点存储的数据不宜过大,几k即可 [保存路径再去查询]
  • 节点可以设置权限,来限制用户的访问
  • Zookeeper保证读和写都是原子操作,且每次读写操作都是对数据的完整读取或完整写入
节点类型
  • 持久节点
  • 临时节点
  • 顺序节点
节点属性
  • dataVersion
  • cversion [child]
  • aclVersion [权限]

常用命令

  • 启动:./bin/zkServer.sh start

  • 连接到Zookeeper

    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
    /usr/bin/java
    ZooKeeper JMX enabled by default
    Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
    
    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkCli.sh -server 127.0.0.1:2181
    
  • 查看节点

    [zk: 127.0.0.1:2181(CONNECTED) 3] ls 
    ls [-s] [-w] [-R] path
    [zk: 127.0.0.1:2181(CONNECTED) 4] ls /
    [zookeeper]
    [zk: 127.0.0.1:2181(CONNECTED) 5] ls /zookeeper
    [config, quota]
    [zk: 127.0.0.1:2181(CONNECTED) 6] 
    
    • 查看节点状态:stat /
    [zk: 127.0.0.1:2181(CONNECTED) 6] stat /
    cZxid = 0x0
    ctime = Thu Jan 01 08:00:00 CST 1970
    mZxid = 0x0
    mtime = Thu Jan 01 08:00:00 CST 1970
    pZxid = 0x0
    cversion = -1    //子节点更改的次数
    dataVersion = 0  //数据更改的情况
    aclVersion = 0   //权限修改的情况
    ephemeralOwner = 0x0  //[0是永久节点 其他的是临时节点]
    dataLength = 0
    numChildren = 1  //有几个子节点
    
    • 查看节点的数据和状态:get
    [zk: 127.0.0.1:2181(CONNECTED) 7] get /45
    jj
    
    • 创建、修改、删除节点
    [zk: 127.0.0.1:2181(CONNECTED) 8] create
    create [-s] [-e] [-c] [-t ttl] path [data] [acl]
    
    //创建
    [zk: 127.0.0.1:2181(CONNECTED) 9] create /imooc2
    Created /imooc2
    [zk: 127.0.0.1:2181(CONNECTED) 10] create /imooc3 123
    Created /imooc3
    [zk: 127.0.0.1:2181(CONNECTED) 11] get /imooc3
    123
    
    [zk: 127.0.0.1:2181(CONNECTED) 12] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x5
    mtime = Sun May 05 01:41:21 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 0
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    
    //修改
    [zk: 127.0.0.1:2181(CONNECTED) 13] set /imooc3 456
    [zk: 127.0.0.1:2181(CONNECTED) 14] get /imooc3
    456
    [zk: 127.0.0.1:2181(CONNECTED) 15] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x6
    mtime = Sun May 05 01:42:16 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 1
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    

高级命令

创建节点的高级功能

  • 创建顺序节点 [-s] (会戴上序号)
[zk: 127.0.0.1:2181(CONNECTED) 16] create /imooc4
Created /imooc4
[zk: 127.0.0.1:2181(CONNECTED) 17] create -s /imooc4 /s
Created /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 18] create -s /imooc4 /s
Created /imooc40000000004
  • 临时节点

    ephemeralOwner = 0x0 [0是永久节点 其他的是临时节点]

[zk: 127.0.0.1:2181(CONNECTED) 22] create /imooc
Created /imooc
[zk: 127.0.0.1:2181(CONNECTED) 23] create -e /imooc/tmp 123
Created /imooc/tmp
[zk: 127.0.0.1:2181(CONNECTED) 24] stat /imooc/tmp
cZxid = 0xd
ctime = Sun May 05 01:48:50 CST 2024
mZxid = 0xd
mtime = Sun May 05 01:48:50 CST 2024
pZxid = 0xd
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x10085ad02c90001  //[0x0是永久节点 其他的是临时节点]
dataLength = 3
numChildren = 0
  • 乐观锁
[zk: 127.0.0.1:2181(CONNECTED) 27] set /imooc 6
[zk: 127.0.0.1:2181(CONNECTED) 28] get /imooc
6
[zk: 127.0.0.1:2181(CONNECTED) 29] stat /imooc
cZxid = 0xc
ctime = Sun May 05 01:48:46 CST 2024
mZxid = 0xe
mtime = Sun May 05 01:51:15 CST 2024
pZxid = 0xd
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 1 
//set -v 1 /imooc 9 是因为上面 dataVersion = 1  指定条件版本更新
[zk: 127.0.0.1:2181(CONNECTED) 35] set -v 1 /imooc 9  
[zk: 127.0.0.1:2181(CONNECTED) 36] get /imooc
9
  • 删除命令
[zk: 127.0.0.1:2181(CONNECTED) 38] delete
delete [-v version] path //也可以按照版本号去删除
[zk: 127.0.0.1:2181(CONNECTED) 42] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000003, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 43] delete /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 44] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 45] 

Watcher机制

  • 触发器、监督者
  • 使用场景:统一资源配置 [发生变化时 会給所有监听客户端发送信息]
Watcher事件类型
EventType 触发条件
NodeCreated (节点创建) Watcher监听的对应数据节点被创建
NodeDeleted (节点删除) Watcher监听的对应数据节点被删除
NodeDataChanged(节点数据修改) Watcher监听的对应数据节点的数据内容发生变更
NodeChildrenChanged(子节点变更) Watcher监听的对应数据节点的子节点列表发生变更
ACL
  • access control list 权限控制
  • 它使用权限位来允许/禁止对话节点及其所作用域的各种操作
  • ACL仅与特定的znode有关,与子节点无关
Scheme
  • ACL:[scheme采用的权限机制:id用户:permissions权限组合字符串]
  • world
  • auth [认证登录]
  • digest [密文加密]
  • ip [只允许特定ip访问]
  • super [超级权限]
权限字符串crdwa
  • Create
  • Read
  • Delete
  • Write
  • Admin
[ACL]权限使用场景
  • 区分开发/测试/运维环境,防止误操作
  • 可以针对不同IP而产生具体的配置,更安全

Java原生客户端连接到zookeeper [ZK]

  • 利用ZK原生的Java的API
  • 利用Apache Curator作为客户端来操作ZK
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>zk-practicer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.0</version>
        </dependency>
    </dependencies>

</project>
com/imooc/zkjavaapi/ZKConnect.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.omg.CORBA.TIMEOUT;

import java.io.IOException;

/**
 * 连接到ZK服务端,打印连接状态
 */
public class ZKConnect implements Watcher {
    public static final String SERVER_PATH="47.98.225.105:2181";
    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args) throws IOException, InterruptedException {
        //后面new的相当于把这个作为参数传递进去
        //客户端和服务端是异步连接,连接成功之后,客户端会收到watcher通知
        //connectString:服务器的IP+端口号
        //sessionTImeout:超时时间
        //watcher:接收通知事件
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKConnect());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        System.out.println("收到了通知" + watchedEvent);
    }
}
===========================================================
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.free=466MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.max=7209MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.total=487MB
17:35:46 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=5000 watcher=com.imooc.zkjavaapi.ZKConnect@7591083d
17:35:46 INFO common.X509Util: Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation
17:35:46 INFO zookeeper.ClientCnxnSocket: jute.maxbuffer value is 1048575 Bytes
17:35:46 INFO zookeeper.ClientCnxn: zookeeper.request.timeout value is 0. feature enabled=false
客户端开始连接ZK服务器了
CONNECTING
17:35:46 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:35:46 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:35:46 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:50517, server: 127.0.0.1/127.0.0.1:2181
17:35:46 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} %p %c{2}: %m%n
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 4147.

windows环境下安装zookeeper教程详解(单机版)_windows zooke-CSDN博客

用代码对节点进行操作

com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:57:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:57:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:57:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57443, server: 127.0.0.1/127.0.0.1:2181
17:57:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710008, negotiated timeout = 5000
/imooc-create-node2
==========================================================================
==========================================================================
 /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
//        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
//                CreateMode.PERSISTENT));
//        zk.setData("/imooc-create-node", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
    }
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:58:09 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:58:09 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:58:09 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57766, server: 127.0.0.1/127.0.0.1:2181
17:58:09 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710009, negotiated timeout = 5000
imooc2
version版本不一样 保证不做修改
com/imooc/zkjavaapi/ZKOperator.java
修改值 让版本变成1
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data)); 
----------------------------------------------------------------------------
Exception in thread "main" org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for /imooc-create-node2
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:122)
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:54)
    at org.apache.zookeeper.ZooKeeper.setData(ZooKeeper.java:2551)
    at com.imooc.zkjavaapi.ZKOperator.main(ZKOperator.java:41)

报错=>版本不一致
===========================================================================
[修改]
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
----------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:01:35 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:01:35 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:01:35 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:58870, server: 127.0.0.1/127.0.0.1:2181
18:01:35 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000d, negotiated timeout = 5000
imooc3 [修改成功]
删除节点 [引入回调函数+休眠]
com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;

import com.imooc.zkjavaapi.callback.DeleteCallBack;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        zk.create("/imooc-create-node3", "imooc3".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
//        zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
//        byte[] data = zk.getData("/imooc-create-node2", null, null);

        String ctx = "删除成功"; //把ctx的内容代入到DeleteCallBack()里面去运行
        zk.delete("/imooc-create-node3",0,new DeleteCallBack(),ctx);
        Thread.sleep(2000);
//        System.out.println(new String(data));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:10:01 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:10:01 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:10:01 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:2600, server: 127.0.0.1/127.0.0.1:2181
18:10:01 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000f, negotiated timeout = 5000
删除节点/imooc-create-node3
删除成功
com/imooc/zkjavaapi/callback/DeleteCallBack.java
package com.imooc.zkjavaapi.callback;

import org.apache.zookeeper.AsyncCallback;

/**
 * 删除后运行的方法
 */
public class DeleteCallBack implements AsyncCallback.VoidCallback {
    @Override
    public void processResult(int rc, String path, Object ctx) {
        System.out.println("删除节点" + path);
        System.out.println((String)ctx);
    }
}

处理Watcher事件

com/imooc/zkjavaapi/ZKGetNode.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * 和节点相关:是否存在,获取数据,加上Watch
 */
public class ZKGetNode implements Watcher {
    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    //这个是门闩
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKGetNode());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());

//        Stat exists = zk.exists("/imooc-create-node", false);//不需要额外监听
//        if (exists != null){
//            System.out.println("节点的版本为: "+exists.getVersion());
//        }else{
//            System.out.println("该节点不存在");
//        }
        zk.getData("/imooc-create-node", true, null);
        countDownLatch.await();
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeChildrenChanged){
            System.out.println("数据被改变");
            countDownLatch.countDown();
        }
        System.out.println("收到了通知" + event);
    }
}
--------------------------------------------------------------------------------
在运行的情况下 去cmd中 修改
[zk: localhost:2181(CONNECTED) 1] set /imooc-create-node 11
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:52:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:52:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:52:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:16204, server: 127.0.0.1/127.0.0.1:2181
18:52:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
数据被改变
收到了通知WatchedEvent state:SyncConnected type:NodeDataChanged path:/imooc-create-node

用Curator操作ZK

原生的Java的API的缺点
  • 不支持连接超时后的自动连接
  • Watcher注册一次后会失效
  • 不支持递归创建节点
利用Apache Curator
  • 解决了Watcher注册一次后会失效的问题
  • API更加简单易用,提供了工具类
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

/**
 * 用Curator来操作ZK
 */
public class CuratorTests {
    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        String path = "/curator";
        String data = "test";
        client.create().withMode(CreateMode.PERSISTENT).forPath(path,data.getBytes());
        byte[] bytes = client.getData().forPath(path);
        System.out.println(new String(bytes));
    }
}
更改高级一点!!!【添加+修改+删除】
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import com.sun.net.httpserver.Authenticator.Retry;
import java.text.MessageFormat;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher.Event.EventType;

/**
 * 描述:     用Curator来操作ZK
 */
public class CuratorTests {

    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        String path = "/curator2";

        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        client.getCuratorListenable().addListener((CuratorFramework c, CuratorEvent event) -> {
            switch (event.getType()) {
                case WATCHED:
                    WatchedEvent watchedEvent = event.getWatchedEvent();
                    if (watchedEvent.getType() == EventType.NodeDataChanged) {
                        System.out.println(new String(c.getData().forPath(path)));
                    }
            }
        });
        String data = "test";
        String data2 = "test2";
        //添加
        client.create().withMode(CreateMode.PERSISTENT).forPath(path, data.getBytes());

        byte[] bytes = client.getData().watched().forPath(path);
        System.out.println(new String(bytes));
        //更改
        client.setData().forPath(path, data2.getBytes());
        //删除
        client.delete().forPath(path);
        Thread.sleep(2000); //保证足够时间运行成功
    }
}
-------------------------------------------------------------------------------------
19:44:41 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
19:44:41 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
19:44:41 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:32984, server: 127.0.0.1/127.0.0.1:2181
19:44:41 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0005, negotiated timeout = 40000
19:44:41 INFO state.ConnectionStateManager: State change: CONNECTED
test
test2

Dubbo [RPC远程过程调用]

  • 初识Dubbo
  • RPC介绍
  • Dubbo工作原理
  • 案例实操:项目编写
  • 整合Dubbo和Zookeeper
  • 实现服务间调用

初始Dubbo

Dubbo是什么
  • 轻量级、高性能的RPC框架
  • 并不是要成为一个微服务的全面解决方案
  • 以Java语言而出名
Dubbo现状
  • 全称是Apache Dubbo
  • 微店、网易云音乐、滴滴、中国电信、中国人寿
  • star有30K+个,fork有20K+个
Dubbo的故事
  • 09年开始做,做的第一个版本
  • 10年初的时候,架构升级,Dubbo2.0
  • 开源
  • one company战略
  • 合到HSF去
  • 第3节点,捐给Apache
开源的理解
  • 共同成长、巨人的肩膀上
  • 演化慢、不断革新、很强大的生命力
  • 突破任何的束缚,突破任何的常规,包容和开放

RPC介绍

  • RPC ——远程过程调用
  • 早期单机时代:IPC
  • 网络时代:把IPC扩展到网络上,这就是RPC
  • 实现RPC很头疼,于是有了RPC框架
  • 调用其他机器上的程序和调用本地的程序一样方便
常见的RPC框架
  • 阿里的Dubbo
  • 新浪的Montan
  • Facebook的Thrift
  • 各个框架都有其各自的优缺点
HTTP和RPC对比
  • 普通话[通用] 与 方言[企业内部]
  • 普通话本质上也是一种方言,只不过它是官方的方言
  • 传输效率
    • RPC定制自己传输请求让传输效率更高
    • HTTP会包含一些无用的内容效率较低
  • 性能消耗,主要在于序列化和反序列化的耗时
  • 负载均衡

Dubbo工作原理

一旦注册中心的信息有变化的时候会主动推送信息
  • 服务容器负责启动,加载,运行服务提供者
  • 服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务提供者在启动时,向注册中心订阅自己所需的服务
  • 注册中心返回服务提供者地址列表给消费者
  • 从提供者地址列表中,选一台提供者进行调用
  • 定期发送一次统计数据到监控中心

模块 说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的控制中心
Container 服务运行容器

服务提供者开发

案例实操
  • 引入依赖
  • 添加注解
  • 整合Dubbo和Zookeeper
pom.xml[dubbo-practice]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>producer</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>dubbo-practice</artifactId>
    <version>0.0.1</version>
    <name>dubbo-practice</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.12.RELEASE</spring-boot.version>
        <dubbo.version>2.7.4.1</dubbo.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Apache Dubbo  -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-dependencies-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>javax.servlet</groupId>
                        <artifactId>servlet-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>log4j</groupId>
                        <artifactId>log4j</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
pom.xml[dubbo-practice-producer]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>producer</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Dubbo Spring Boot Starter -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <!-- Zookeeper dependencies -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-zookeeper</artifactId>
            <version>${dubbo.version}</version>
            <type>pom</type>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Web 功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MySQL connector, 需要与 MySQL 版本对应 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- MyBatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
</project>
com/imooc/producer/service/CourseListService.java
package com.imooc.producer.service;

import com.imooc.producer.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/producer/entity/Course.java
package com.imooc.producer.entity;

import java.io.Serializable;

/**
 * 描述:     Course实体类
 */
public class Course implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    //1上架,0下架
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", valid=" + valid +
                '}';
    } Getter+Setter
}
com/imooc/producer/service/impl/CourseListServiceImpl.java
package com.imooc.producer.service.impl;

import com.imooc.producer.entity.Course;
import com.imooc.producer.mapper.CourseMapper;
import com.imooc.producer.service.CourseListService;
import java.util.List;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 描述:     课程列表服务实现类
 */
@Service(version = "${demo.service.version}")
public class CourseListServiceImpl implements CourseListService {

    @Autowired
    CourseMapper courseMapper;

    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/producer/mapper/CourseMapper.java
package com.imooc.producer.mapper;

import com.imooc.producer.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FORM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties
demo.service.version=1.0.0

#server.port=8081

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_prepare?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-list

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache

mybatis.configuration.map-underscore-to-camel-case=true

dubbo.scan.base-packages=com.imooc.producer.service.impl
com/imooc/producer/DubboProducerApplication.java
package com.imooc.producer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@EnableAutoConfiguration
public class DubboProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboProducerApplication.class, args);
    }
}

服务消费方开发

查看PID为8080:netstat -ano|findstr 8080
杀死进程:taskkill /pid 查询的PID /f

pom.xml[dubbo-practice-consumer]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>dubbo-practice</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>concumer</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <!-- Dubbo Spring Boot Starter -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.7.4.1</version>
    </dependency>

    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo</artifactId>
    </dependency>
    <!-- Zookeeper dependencies -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-dependencies-zookeeper</artifactId>
      <version>${dubbo.version}</version>
      <type>pom</type>
      <exclusions>
        <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- Web 功能 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MySQL connector, 需要与 MySQL 版本对应 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- MyBatis依赖-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.imooc</groupId>
      <artifactId>producer</artifactId>
      <version>0.0.1</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>
com/imooc/consumer/service/CoursePriceService.java
package com.imooc.consumer.service;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import java.util.List;

/**
 * 描述:     课程价格服务
 */
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);

    List<CourseAndPrice> getCoursesAndPrice();
}
com/imooc/consumer/service/impl/CoursePriceServiceImpl.java
package com.imooc.consumer.service.impl;

import com.imooc.consumer.dao.CoursePriceMapper;
import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import com.imooc.producer.entity.Course;
import com.imooc.producer.service.CourseListService;
import java.util.ArrayList;
import java.util.List;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 描述:     课程 价格服务
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;

    @Reference(version = "${demo.service.version}")
    CourseListService courseListService;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrices(courseId);
    }

    @Override
    public List<CourseAndPrice> getCoursesAndPrice() {
        List<CourseAndPrice> courseAndPriceList = new ArrayList<>();
        List<Course> courseList = courseListService.getCourseList();
        for (int i = 0; i < courseList.size(); i++) {
            Course course = courseList.get(i);
            if (course != null) {
                CoursePrice price = getCoursePrice(course.getCourseId());
                if (price != null && price.getPrice() > 0) {
                    CourseAndPrice courseAndPrice = new CourseAndPrice();
                    courseAndPrice.setId(course.getId());
                    courseAndPrice.setCourseId(course.getCourseId());
                    courseAndPrice.setName(course.getName());
                    courseAndPrice.setPrice(price.getPrice());
                    courseAndPriceList.add(courseAndPrice);
                }
            }
        }
        return courseAndPriceList;
    }
}
com/imooc/consumer/dao/CoursePriceMapper.java
package com.imooc.consumer.dao;

import com.imooc.consumer.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 描述:     Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {

    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrices(Integer courseId);
}
com/imooc/consumer/entity/CourseAndPrice.java
package com.imooc.consumer.entity;

import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CourseAndPrice implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    Integer price;

    @Override
    public String toString() {
        return "CourseAndPrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    } Getter+Setter
}
com/imooc/consumer/entity/CoursePrice.java
package com.imooc.consumer.entity;


import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CoursePrice implements Serializable {

    Integer id;
    Integer courseId;
    Integer price;
} Getter+Setter
com/imooc/consumer/controller/CoursePriceController.java
package com.imooc.consumer.controller;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:CoursePriceController
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;


    @GetMapping({"/price"})
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        if (coursePrice != null) {
            return coursePrice.getPrice();
        } else {
            return -1;
        }
    }


    @GetMapping({"/coursesAndPrice"})
    public List<CourseAndPrice> getcoursesAndPrice() {
        return coursePriceService.getCoursesAndPrice();
    }
}
application.properties
demo.service.version=1.0.0

server.port=8084

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-price

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache
com/imooc/consumer/DubboConsumerApplication.java
package com.imooc.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@SpringBootApplication
public class DubboConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboConsumerApplication.class, args);
    }
}
案例实操总结
  • 自动检查zk和依赖的服务
  • dubbo.scan.base-packages配置
  • 实现服务间调用

面试课

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式
  • 面试避坑指南
  • 重要的软实力

Spring、Spring Boot和Spring Cloud的关系?

  • Spring最初利用IOCAOP解耦
  • 按照这种模式搞了MVC框架 [之后就配置太繁琐了]
  • 写了很多样板代码很麻烦,就有了Spring Boot
  • Spring Boot是在强大的Spring帝国发展起来的,发明Spring Boot是为了让人们更好更高效的使用Spring,Spring Boot理念是约定优于配置
  • Spring Cloud是在Spring Boot基础上诞生的 [一系列框架的有序集合]

Spring Boot如何配置多环境

  • 开发、测试、预发、生产

面试官你好,我这边平时是会使用多套环境,比如说”开发、测试、预发、生产”环境。
开发环境通常在本地,它所连接的数据库也是专门用于开发的,里面的数据也是一定情况下算出来的,因为并不需要在开发环境的情况下保证数据的完全精确,为了开发效率的提高,我们通常造一些模拟的数据,通常开发完后我们要把程序部署到测试环境,因为测试环境通常是公司所提供的服务器,而开发环境通常是我们本机,对于本机而言如果关闭或关机后别人就无法访问了,但是测试的同学工作时间不一定能和开发的同学一致,如果把程序关掉了他们就没办法测试了。我们需要给测试同学提供一套稳定的环境去测试。而且有的时候会同时开发多种功能,前一个功能开发完了需要去测试,这个时候就要去开发新的功能了,此时本地的代码已经发生了变化,如果把开发环境当成测试环境的话会发生很多问题,它实际测试的和我们想要测试的不是同一套代码,正是这个原因测试环境是必不可少的,需要用一台稳定的服务器把我们开发好的部署上测试环境中去,这样的话无论电脑是否关机都不会影响测试人员的进度。但是在测试环境的数据库往往可以和开发环境的保持一致可以允许公用同一个数据。
预发环境是预备发布,和真正的线上环境高度统一,和测试环境的区别:
1.网络隔离 为了保证线上环境的稳定会采取环境隔离,在本地或者测试环境下是没有办法访问到预发环境的机器,不可直接访问。在预发环境通常采用真实的数据库去测试。在测试环境并不能把所有问题都测试出来,所以在测试环境中无法测试到的问题在预发环境就可以暴露出来了,有时候在测试环境中模拟的数据不是准确,比如模拟一个商品详情,报的是50个字,最后发现真实情况是100个字,就能看到数据库大小不够,再次比如测试的是整数,到真实环境中发现是小数。隔离+数据验真
生产环境是真实对外的数据,也会有很多流量进来,直接面向所有用户,也有并发问题,要确保数据稳定

  • 提供多套配置文件

在发布到某个环境之前,不建议把配置文件全部删除替换,有可能漏了文件导致了错误的替换,如果发布环境是测试环境的数据库,有可能会产生对外暴露的是测试环境的情况,这是很严重的事故

  • 通过改变application里的profiles.active值来加载对应的环境
com/imooc/profiles/ProfilesApplication.java
package com.imooc.profiles;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProfilesApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProfilesApplication.class, args);
    }
}
application.properties

spring.profiles.active=prod
application-pre.properties

spring.profiles.active=test
server.port=8082
application-prod.properties

spring.profiles.active=test
server.port=8083
application-test.properties

spring.profiles.active=test
server.port=8081

实际工作中,如何全局处理异常?

  • 为什么异常需要全局处理?不处理行不行?

如果我们不进行处理的话,异常可能会把整个堆栈抛出去,一旦发生异常,用户或者别用用心的黑客可以看到详细的异常发生情况,包含详细的错误信息和代码的行数,这样的话对方可以利用一个漏洞进行不同的尝试,而且可以顺藤摸瓜分析出更多潜在的风险,最终把系统攻击破,异常是必须处理的。
但为什么要全局处理呢?电商项目→exception→GlobalExceptionHandler

  • GlobalExceptionHandler [使用全局处理]
    识别到什么异常,调用什么其处理器。写了全局异常处理器,轻松的针对不同的异常做出定制化的解决方案,不但增加了安全性,对用户也是友好的
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常 所有异常的父类
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }
    
    // 处理自己所定义的异常 用户/密码不能为空......
    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }

    //  39.处理方法参数不合规的情况
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException: ", e);
        return handleBindingResult(e.getBindingResult());
    }
//  40.处理返回异常的ApiRespond 41去pom引入Swagger自动生成API文档
    private ApiRestResponse handleBindingResult(BindingResult result){
//  把异常处理为对外暴露的提示
        List<String> list = new ArrayList<>();
        if (result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors) { //itli快速  对着for按alt+回车 改成增强for
                String message = objectError.getDefaultMessage();
                list.add(message);
            }
        }
        if (list.size() == 0){
            return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
        } //list.toString()生成所创建的异常描述信息
        return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
    }
}

线程如何启动?

Thread.start.run

  • 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?

因为它只是一个普通的java代码,而不会真正的启动一个线程,调用一次run()方法只执行一次,而且是在主线程执行的,就没有起到任何创建线程的效果了。
如果选择start方法的话会在后台执行很多操作,比如去申请一个线程、让子方法去执行run()里的内容,而且还包括执行之后的对线程状态的调整。所以说表面上是相同都是执行一段代码,但是实际上是不同的。

  • 两次调用start()方法会出现什么情况?

两次调用start()方法会报异常,异常类型叫做IllegalThreadStateException,在start()的时候首先会进行线程状态的检测只有是new的时候才能正常启动,不允许启动两次

com/imooc/interniew/StartTwice.java
package com.imooc.interniew;

/**
 * 描述:     两次启动线程
 */
public class StartTwice {

    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        thread.start();
    }
}
======================== 报错信息  ==========================
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:705)
    at com.imooc.interniew.StartTwice.main(StartTwice.java:11)
Thread.java
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

实现多线程的方法有几种?

  • 方法一:实现Runnable接口
com/imooc/interniew/createthreads/RunnableStyle.java
package com.imooc.interniew.createthreads;

import java.util.concurrent.Callable;

/**
 * 描述:     用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable {
//new里new的意思是 把这个Runnable类作为参数传进Thread里面
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Runnable方法实现线程");
    }
}
  • 方法二:继承Thread类
package com.imooc.interniew.createthreads;

import java.util.Timer;
import java.util.TimerTask;

/**
 * 描述:     利用定时器新建线程
 */
public class TimerDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}

两种方式的对比

方法1(实现Runnable接口更好)

实现多线程——常见面试问题
  • 实现Runnable接口和继承Thread类哪种方式更好?

    • 从代码架构角度

    本意是想让我们的执行类和任务的具体内容解耦,关系不那么密切,从架构角度好
    ★ Runnable具体描述的是工作的内容和线程的启动没有什么关系
    ★ Thread是维护整个线程的: 线程的启动、线程状态更改、线程结束,这两个本身的任务很分明的,不应该过度耦合[未来会发生很难扩展的问题]

    • 新建线程损耗

    ★ Runnable 在线程池更高级的用法中,一定不是每个任务都去新建一个线程的,为了提高整体的效率会让有限数量的线程由我们自己来确定,10个线程可以运行成千上万个任务。减少了新建线程的损耗。
    可以把任务作为一个参数直接传递给线程池,线程池里面用固定的线程去执行任务不需要每次都新建和销毁线程,这样大大降低了线程的开销。

    ★ Runnable 如果用这个不得不去把线程损耗承担起来,有的时候run方法执行的比较少,开销的少比不上新建线程的开销[捡了芝麻丢了西瓜]。

    • Java不支持双继承

    public class ThreadStyle extends Thread,Date (×)
    Class cannot extend multiple classes

    从语法的角度不允许继承多个类,一旦选定了一个父类就无法更改了[一辈子就被定死了]。在创建线程起就限制了代码的可扩展性,如果实现Runnable接口就不会出现这个问题,实现接口并不仅仅只能实现一个,实现接口后还可以继承类

    public class RunnableStyle extends Thread implements Runnable,Callable

两种方法的本质对比

方法一:最终调用target.run();

此方法本质是传入类后调用!
Ctrl+F12可以精确查找方法
@Override
public void run(){
if(target != null){
target.run()
}
}
而target是什么呢?实际上就是我们写的
@Override
public void run() {
System.out.println(“用Runnable方法实现线程”);
}

方法二:run()整个都被重写

整个重写代码

若同时使用这两种方法会发生什么?

  • 从面向对象的思想去考虑
com/imooc/interniew/createthreads/BothRunnableThread.java
package com.imooc.interniew.createthreads;
/**
 * 描述:     同时使用RUNNABLE和Thread两种方式实现线程
 */
public class BothRunnableThread {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口的方式");
            }
        }) {
            @Override
            public void run() {
                System.out.println("我来自Thread");
            }
        };
        t1.start();
    }
}
=========================================================
我来自Thread
---------------------------------------------------------
因为 run重写会被覆盖!!子类覆盖父类时 实行子类方法
@Override
public void run(){
    if(target != null){
        target.run()
    }
}
其他观点分析
  • 线程池创建线程也算是一种新建线程的方式 [把那两种方式进行包装]

  • 通过Callable创建线程,也算是一种新建线程的方式

  • 定时器[方法二:extends Thread]

    package com.imooc.interniew.createthreads;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * 描述:     利用定时器新建线程
     */
    public class TimerDemo {
    
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName());
            Timer timer = new Timer();
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }, 1000, 1000);
        }
    }
    =======================================================
    main     //两个不一致证明新建了一个线程
    Timer-0
    Timer-0
    ......
    
  • 匿名内部类

  • Lambda表达式

实现多线程——常见面试问题
  • 有多少种实现线程的方法?5点思路
    • 不同的角度看,会有不同的答案
    • 经典答案是
    • 我们看原理,两种本质都是一样的
    • 具体展开说其他方式
总结:最精准的描述

准确地讲:创建线程只有一种方法那就是构造Thread类,而实现线程的执行单元有两种方式
★ 方法一:实现Runnable接口的run方法,并把Runnable实例传給Thread类
★ 方法二:重写Thread的run方法(继承Thread类)
多线程的实现方法,在代码种写法千变万化,但其本质万变不离其宗

线程的生命周期是什么?

线程有几种状态?
  • 有哪6种状态
  • 每个状态是什么含义?
  • 状态间的转化?
  • 阻塞状态是什么?
每个状态是什么含义?
  • New
  • Runnable [从new到调用start方法]
  • Blocked [线程状态由sychronized修饰]
  • Waiting
  • Timed Waiting
  • Terminated

状态转换的注意点和阻塞

com/imooc/interniew/NewRunnableTerminated.java
package com.imooc.interniew;

/**
 * 描述:     演示New、Runnable、Terminated状态。
 */
public class NewRunnableTerminated {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread();
        //打印出NEW的状态 线程被创建但没有启动会打出new状态
        System.out.println(thread.getState());
        thread.start();
        //打印出Runnable状态 线程被启动后打印runnable状态
        System.out.println(thread.getState());
        Thread.sleep(100);
        //打印出TERMINATED状态 打印terminate状态
        System.out.println(thread.getState());
    }
}
=================================================================================
NEW
RUNNABLE
TERMINATED

Process finished with exit code 0
com/imooc/interniew/BlockedWaitingTimedWaiting.java
package com.imooc.interniew;

/**
 * 描述:     展示Blocked、Waiting、Timed_Waiting状态
 */
public class BlockedWaitingTimedWaiting implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new BlockedWaitingTimedWaiting();
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread t2 = new Thread(runnable);
        t2.start();
        Thread.sleep(10);
        //打印Timed_Waiting状态,因为正在执行Thread.sleep(1000);
        System.out.println(t1.getState());
        //打印出BLOCKED状态,因为t2拿不到synchronized锁[线程1还在休眠]
        System.out.println(t2.getState());

        Thread.sleep(1300);
        //打印出WAITING状态,以为执行了wait()
        System.out.println(t1.getState());
    }

    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() { //锁!!
        try {
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
=================================================================================
TIMED_WAITING
BLOCKED
WAITING
阻塞状态
  • 一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态
  • 不仅仅是Blocked

分布式面试题

什么是分布式
  • 饭店厨师的例子
    • 一个厨师
    • 多个厨师
    • 术业有专攻:配菜师、洗菜工
  • 实际项目的演进过程
    • 一个项目,大而全
    • 多台机器,部署同样的应用
    • 分布式:权限系统、员工系统、请假系统
分布式和单体结构哪个更好?[脱离业务场景和发展阶段的空谈就是耍流氓]
传统单体架构 分布式架构
新人的学习成本 业务逻辑成本高 架构逻辑成本高
部署、运维 容易 发布频繁、发布顺序复杂、运维难
隔离性 一损俱损,殃及鱼池 故障影响范围小

CAP理论是什么?[只选其二 三者不可兼得]

  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果
  • A(Availability, 可用性):非故障节点应该在合理的时间内做出合理的响应
  • P(Partition tolerance, 分区容错性):当出现网络分区现象后,系统能够继续运行
CAP怎么选?
  • CP或者AP
  • 在什么场合,可用性高于一致性?

为什么需要Docker?

  • Docker:用来装程序以及环境的容器
  • 环境配置的难题
  • 虚拟机
Docker的用途是什么?
  • 提供统一的环境
  • 提供快速拓展、弹性伸缩的云服务
  • 防止其他用户的进程把服务器资源占用过多

Docker的架构是什么样的?

Docker的网络模式有哪些?
  • Bridge [桥接 用外面主机的端口号映射到里面的端口号 实现了一座桥]
  • Host [里面的容器不会获得独立的网络配置 不会使用虚拟网卡ip 而是使用宿主机上的ip和端口号]
  • None [不需要网络模式]

Nginx的适用场景有哪些?

  • HTTP的反向代理服务器

  • 动态静态资源分离

    • 不分离会变慢
    • 静态资源无需经过Tomcat,Tomcat只负责处理动态请求
    • 后缀为gif的时候,Nginx会直接获取到当前请求的文件并返回
    • 静态资源服务器

Nginx常用命令有哪些?

/usr/sbin/nginx 启动
-h 帮助
-c 读取指定的配置文件
-t 测试
-v 版本
-s信号
  stop 立即停止(不再接收任何请求立刻停止)  
  quit 优雅停止(不接收了但目前的请求要作完)
  reload 重启
  reopen 更换日志文件

Zookeeper有哪些节点类型?

  • 持久节点
  • 临时节点
  • 顺序节点

为什么要用消息队列?什么场景用?

  • 系统解耦
  • 异步调用
  • 流量削峰
消息队列RabbitMQ核心概念?

同一个RabbitMQ的Server下建立不同的虚拟主机,他们之间是相互独立的,用于不同的业务线。

交换机工作模式有哪4种?
  • fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的

  • direct:根据RoutingKey匹配消息路由到指定队列 [消费者接收消息不一致]

  • topic:比如消息严重性怎么样、只想记录error模块的用户信息

    • ***** 可以替代一个单词

    • # 可以替代零个或多个单词

  • headers:根据发送消息内容中的headers属性来匹配

微服务面试题

微服务有哪两大门派?

  • Spring Cloud:众多子项目
  • dubbo:高性能、轻量级的开源RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
Spring Cloud核心组件有哪些?
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用 Spring Cloud Netflix Feign
服务网关 Spring Cloud Netflix Zuul
断路器 Spring Cloud Netflix Hystrix
画一下Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

负载均衡的两种类型是什么?
  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡有哪些策略?
  • RandomRule表示随机策略
  • RoundRobinRule表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
为什么需要断路器?

防止线程突然卡住,当发现某个模块不可用时,把它摘除不影响主要流程。

为什么需要网关?
  • 签名校验、登录校验冗余问题
  • 统一对外,安全 [对恶意IP进行拦截 打出日志]
Dubbo的工作流程是什么?

彩蛋:学习编程知识的优质路径

  • 宏观上
  1. 并不是靠工作年限,有的人工作5年技术却还是只懂皮毛
  2. 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高
  3. 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等
  4. 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程种,进步是飞速的
  5. 关心产品,关心业务,而不是只写代码
  • 微观上
  1. 系统化的学习 看经典的书籍
  2. 看官方文档
  3. 自己动手写代码,尝试应用到项目中
  4. 不理解的内容参考多个知识来源,综合判断
  5. 学习开源项目,总结代码

Synchronized和Lock

Lock简介、地址、作用
  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和Synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来替代Synchronized的,而是当使用Synchronized不合适或不满足要求的时候,来提供高级功能
  • Lock接口最常见的实现类是ReentrantLock
  • lock()、tryLock()、tryLock(long time, TimeUnit unit)locakInterruptibly()
lock()
  • lock()就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
  • Lock不会像Synchronized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这就会带来很大隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相对比lock,这样的方法显然功能更加强大了,我们可以根据是否能获取到锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time, TimeUnit unit):超时就放弃

locakInterruptibly():相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

unlock():解锁 [最应该写在try…finally里面]

Synchronized和Lock有什么不同?

相同点:
  • 保障资源线程的安全:目的和作用都是为了 保障资源线程的安全
    [使用Synchronized后被保护的代码块最多只有一个线程可以访问]
  • 可重入 [不然就必须在获得第二个锁前释放]
com/imooc/interniew/Reentrant.java
package com.imooc.interniew;

/**
 * 描述:     synchronized可重入
 */
public class Reentrant {

    public synchronized void f1() {
        System.out.println("f1方法被运行了");
        f2();
    }

    public synchronized void f2() {
        System.out.println("f2方法被运行了");
    }

    public static void main(String[] args) {
        Reentrant reentrant = new Reentrant();
        reentrant.f1();
    }
}
========================================================
f1方法被运行了
f2方法被运行了
  • ReentrantLock [实现了Lock接口]
不同点:
  • 用法
    • Synchronized用在方法上、用在同步代码块上 [隐式]
    • Lock必须使用lock方法加锁unlock方法解锁 [显式]
  • 加解锁顺序不同
    • Synchronized是java内部控制,自动加解锁
    • Lock可以手动调节
  • Synchronized锁不够灵活
    • Synchronized获得了一个锁 其他的只能等待
    • Lock获得锁很灵活 可以随时调整
  • 性能区别
    • Synchronized由差到好 目前同等程度的性能

你知道有几种锁?

  • 共享锁[读写锁] 和 独占锁[排他锁]

  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

  • 共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁的作用
  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
  • 一句话总结:要么是一个多个线程同时有读锁,要么一个线程有写锁,但是两者不会同时出现(要么多读,要麽一写)
公平锁 和 非公平锁
  • 公平指的是按照线程请求的顺序,来分配锁
  • 非公平指的是不完全按照请求的顺序,在一定情况下,可以插队
  • 注意:非公平也同样不提倡 ”插队“ 行为,这里的非公平,指的是”在合适的时机”插队,而不是盲目插队
  • 什么是合适的时机呢?
    • 买火车票被插队的例子,排队买的例子
  • 实际情况并不是这样的,java设计者这样设计的目的是为了提高效率
  • 避免唤醒带来的空档期,提升吞吐量
优势 劣势
公平锁 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行
悲观锁 和 乐观锁
  • 是否锁住资源的角度分类
悲观锁
  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • java中悲观锁的实现就是synchronizedLock相关类
乐观锁
  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
在数据库中
  • select for update就是悲观锁
  • version控制数据库就是乐观锁
经典例子
添加一个字段lock_version
先查询这个更新语句的vesion:SELECT * FROM table
然后
UPDATE SET num = 2, 
version = version + 1 WHERE version = 1 AND id = 5
如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理

自旋锁 和 非自旋锁
  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
  • 而为了让当前线程“稍等一下”,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
自旋锁的缺点
  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗CPU,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
可重入锁 和 非可重入锁
  • 什么是可重入 [摇一个号拿N个牌]

  • 好处 [避免死锁]

可中断锁 和 不可中断锁
  • 可中断锁 [可以随时中断]

死锁相关

写一个必然死锁的例子?
什么是死锁?
  • 发生在并发

  • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁

  • 一图胜千言

  • 线程A持有锁1但试图获取锁2 线程B持有锁2但视图获取锁1

多个线程造成死锁的情况
  • 如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁
死锁的影响
  • 死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
    • 数据库中:检测并放弃事务
    • JVM中:无法自动处理

几率不高但危害大
  • 不一定发生,但是遵守墨菲定律
  • 一旦发生,多是高并发场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁
deadlock/DeadLock.java
package deadlock;

/**
 * 描述:     必然发生死锁
 */
public class DeadLock implements Runnable {

    public int flag;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public void run() {
        System.out.println("开始执行");
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
        if (flag == 2) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock r1 = new DeadLock();
        DeadLock r2 = new DeadLock();
        r1.flag = 1;
        r2.flag = 2;
        new Thread(r1).start();
        new Thread(r2).start();
    }
}
====================================== 分析 =======================================
★ 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;
★ 而T1在睡眠的时候另一个flag=2的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;
★ T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定
★ T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定
★ T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁

哲学家就餐问题?

  • 先拿起左手的筷子
  • 然后拿起右手的筷子
  • 如果筷子被人使用了,那就等别人用完
while(true){ //伪代码
    think();
    pick_up_left_fork();
    pick_up_right_fork();
    eat();
    put_down_right_fork();
    put_down_left_fork();
}
package deadlock;

/**
 * 描述:     哲学家就餐问题导致的死锁
 */
public class DiningPhilosophers {

    public static class Philosopher implements Runnable {

        private Object leftChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        private Object rightChopstick;

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {//五个哲学家方便管理
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i]; //从0开始 i为5 所以底下要加1 但是越界就取余
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            //领导调节(检测与恢复策略) [定期巡视命令哲学家] 让最后一个 跟别人不一样 
            //别人都是先左后右面 它是先后面再左边 避免了环路的形成
            //直接避免死锁发生!!!!!
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}
=============================================================================
哲学家4号 Thinking
哲学家5号 Thinking
哲学家3号 Thinking
哲学家1号 Thinking
哲学家2号 Thinking
哲学家2号 Picked up left chopstick
哲学家3号 Picked up left chopstick
哲学家1号 Picked up left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家4号 Thinking
哲学家3号 Picked up right chopstick - eating
哲学家3号 Put down right chopstick
哲学家3号 Put down left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家3号 Thinking
哲学家2号 Picked up right chopstick - eating
哲学家4号 Thinking
哲学家2号 Put down right chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
.............................
发生死锁的时候哲学家都拿着左边的筷子
原理:Thread.sleep((long) (Math.random() * 10));
random到了一个更大的数
多种解决策略
  • 服务员检查(避免策略) [提前看一看是否发生死锁]
  • 改变一个哲学家拿叉子的顺序(避免策略)
  • 餐票(避免策略)
  • 领导调节(检测与恢复策略) [定期巡视命令哲学家]
实际工程中如何避免死锁
① 设置超时时间
  • Lock的tryLock(long timeout, TImeUnit unit)
  • synchronized不具备尝试锁的能力
  • 造成超时的可能性很多:发生了死锁、线程陷入死循环、线程执行很慢
  • 获取锁失败:打日志、发报警邮件、重启
  • 代码演示:退一步海阔天空
package deadlock;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程1获取到了锁1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程1获取到了锁2");
                            System.out.println("线程1成功获取到了两把锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("线程1尝试获取锁2失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获取到了锁2");

                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2成功获取到了两把锁");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("线程2尝试获取锁1失败,已重试");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
====================================================================================
线程1获取到了锁1
线程2获取到了锁2
线程1尝试获取锁2失败,已重试 【正是有了线程1的释放 才让线程2拿到了锁1】
线程2获取到了锁1
线程2成功获取到了两把锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了两把锁
② 多使用并发类而不是自己设置锁
③ 尽量降低锁的使用粒度:用不同的锁而不是一个锁
④ 如果能使用同步代码块,就不使用同步方法:自己锁定锁对象
⑤ 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践

Hashmap为什么[线程]不安全?

  • i++

    • 第一个步骤是读取
    • 第二个步骤是增加
    • 第三个步骤是保存

    有可能会发生线程不安全的情况

  • 同时put碰撞导致数据丢失

  • 可见性问题无法保证

final的作用是什么?有哪些用法?

  • final修饰变量
  • final修饰方法
  • final修饰
final的作用
  • 早期
    • 锁定
    • final效率:早期的Java实现版本中,会将final方法转为内嵌调用
  • 现在
    • 类防止被继承、方法防止被重写、变量防止被修改
    • 天生是线程安全的,而不需要额外的同步开销
final的3种用法
final修饰变量:赋值时机
  • 属性被声明为final后,该变量则只能被赋值一次。且一旦被赋值,final的变量就不能再被改变,无论如何也不会改变

  • final修饰变量

    • final instance variable (类中的final属性)

      • 第一种是在声明变量的等号右边直接赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static int a = 5;
      
          public static void main(String[] args) {
              FinalVariable.a = 8;
          }
      }
      
      • 第二种就是构造函数中赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public final int a;
      
          public testFinal(int a) {
              this.a = b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 第三种就是在类的初始代码块种赋值(不常用)
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static final int a;
      
          static {
              a = 9;
          }
      
          void testFinal() {
              final int b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 如果不采用第一种赋值方法,那么就必须在第2、3种挑一个来赋值,而不能不赋值,这是final语法所 规定的
    • final static variable (类中的static final属性)

      • 两种赋值时机:除了在声明变量的等号右边直接赋值外,static final变量还可以用static初始代码块赋值,但是不能用普通的初始代码块赋值
    • final local variable (方法中的final变量)

      • 和前两种不同,由于这里的变量是在方法里的,所以没有构造函数,也不存在初始代码块
      • final local variable不规则赋值时机,只要求在使用前必须赋值,这和方法中的非final变量的要求也是一样的
    为什么要规定赋值时机?
    • 如果初始化不赋值,后续赋值,就是从null变成你的赋值,这就违反final不变的规则了
  • final修饰方法

    • 构造方法不允许final修饰
    • 不可被重写,也就是不能被override
    package com.imooc.interniew;
    
    /**
     * 描述:     final修饰方法
     */
    public class FinalMethodDemo{
    
        public void drink() {
        }
    
        public final void eat() {
        }
    }
    
    class SubClass extends FinalMethodDemo {
    
        @Override
        public void drink() {
            super.drink();
        }
    
    //    @Override
    //    public void eat() {
    //        super.drink();
    //    }
    }
    
  • final修饰

    • 不可被继承
    • 例如经典的String类就是final的,我们从见过哪个类是继承String类的

单例模式你会写吗?

  • 什么是单例模式

保证只有一个实例且提供只有一个全局入口

  • 为什么需要单例:节省内存和计算、保证结果正确、方便管理
  • 适用场景
    • 无状态的工具类
    • 全局信息类
  • 单例模式的8种写法
    • 饿汉式(静态常量) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态常量)(可用)
     */
    public class Singleton1 {
    
        private Singleton1() {
    
        }
    
        private final static Singleton1 INSTANCE = new Singleton1();
    // [未达到懒加载] 直接创建出来了
        public static Singleton1 getInstance() {
            return INSTANCE;
        }
    }
    
    • 饿汉式(静态代码块) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态代码块)(可用) // [未达到懒加载]
     */
    public class Singleton2 {
    
        private Singleton2() {
    
        }
    
        static {
            INSTANCE = new Singleton2();
        }
        private final static Singleton2 INSTANCE;
    
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
    
    • 懒汉式(线程不安全) [不可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程不安全)
     */
    public class Singleton3 {
    
        private Singleton3() {
    
        }
    
        private static Singleton3 INSTANCE;
    
        public static Singleton3 getInstance() {
            if (INSTANCE == null) {//第一次访问方法
                INSTANCE = new Singleton3();//初始化
            }
            //此时如果两个线程同时访问,都是null,就创造了两个初始化 违反单例模式
            return INSTANCE; //已经被初始化 就返回
        }
    }
    
    • 懒汉式(线程安全,同步方法) [不推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton4 {
    
        private Singleton4() {
    
        }
    
        private static Singleton4 INSTANCE;
        //synchronized同步关键字 最多一个线程访问
        //不推荐用的原因是因为一旦适用了synchronized同步关键字 线程就要排队 并发量大
        public synchronized static Singleton4 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new Singleton4();
            }
            return INSTANCE;
        }
    }
    
    //方法上不进行同步了
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton5 {
    
        private Singleton5() {
    
        }
    
        private static Singleton5 INSTANCE;
    
        public static Singleton5 getInstance() {
            if (INSTANCE == null) { //此时不会存在两个线程同时出来了
                synchronized (Singleton5.class) {
                    //假如第一个执行完了 第二个进去执行 那么结果还是生成了两个 不符合单例
                    INSTANCE = new Singleton5();
                }
            }
            return INSTANCE;
        }
    }
    
    • 双重检查[推荐用]
      • 新建一个对象,但还未初始化
      • 调用构造函数等来初始化该对象
      • 把对象指向引用
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton6 {
    
        private Singleton6() {//2
    
        }
    
        private static volatile Singleton6 INSTANCE;
    
        public static Singleton6 getInstance() {
            if (INSTANCE == null) {
                synchronized (Singleton6.class) {
                    if (INSTANCE == null) { //3
                        //就不会出现多个结果了
                        INSTANCE = new Singleton6 //1
                    }
                }
            }
            return INSTANCE;
        }
    }
    
    • 静态内部类[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     静态内部类写法(推荐用)
     */
    public class Singleton7 {
    
        private Singleton7() {
    
        }
    
        private static class SingletonInstance {
            private static Singleton7 INSTANCE = new Singleton7();
        }
    
        public static Singleton7 getInstance() {
            return SingletonInstance.INSTANCE;
        }
    }
    
    • 枚举[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     枚举单例模式
     */
    public enum Singleton8 {
        //1.写法简洁
        //2.线程安全
        //3.防止反射
        
        INSTANCE;
    }
    
不同写法对比
  • 饿汉:简单,但是没有lazy loading
  • 懒汉:有线程安全问题
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:最好
单例模式面试常见问题
  • 饿汉式的缺点?[没有懒加载]
  • 懒汉式的缺点?[不可以保证线程安全]
  • 为什么要用double-check?不用就不安全吗?
  • 为什么双重检查模式要用volatile
  • 应该如何选择,用哪种单例的实现方案最好
    • 单元素的枚举类型已经成为实现Singleton的最佳方法
    • 写法简单
    • 线程安全有保障
    • 避免反射破坏单例

面试避坑指南

  • 何时投简历 [Offer数量只会越来越少,越早越好] 秋招7-9月 社招金3银4
  • 信息尽量全面
  • 技术栈契合
  • 慎用”精通“ [对源码有很多熟悉] => 多写熟悉
  • 面试无处不在 [如果没时间可以申请换一个时间节点 并询问对方是否有时间]
  • 提前调试设备
  • 仪容仪表、提前到场 [提前5分钟左右联系面试官]
  • 确认问题 [实在不会可以说思路设想]
  • 问面试官的问题 [提前查公司信息 我了解到我们公司… 可不可以介绍一下… 未来规划… 技术栈…]

哪些软素质值得面试官认可?

  • 基本能力:聆听、沟通表达、学习能力
  • 工作能力:协作执行力、管理能力
  • 个人素质:技术自驱力、韧性、积极开放的心态

面试课总结

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式[高频考点]
  • 面试避坑指南
  • 重要的软实力

2024.5.9 14:34   在创新楼B105-JSP动态网页设计课上  完成了java+4399全部课程的学习

阅读全文

2024万能论据

2024/4/17

健康

1.sth contribute to one's health by giving sb some physical exercise.
  某物能有助于身体健康通过给某人一些身体锻炼
2.sth is harmful to/bad for/detrimental to health.
  某物对健康有害
3.sth exert great pressure on sb.
  某物给某人施加重压
4.sb may become more solitary and even suffer from certain mental illness.
  某人可能变得孤僻甚至产生某些心理疾病
5.Exercise can enhance immune function.
  锻炼可以增强免疫力
6.adopt a healthy and meaningful life style.
  采取一个健康和有意义的生活方式
7.release pressure.
  ★减压 

乐趣

1.sth will make ones's life more enjoyable. That is to say, sth can add color to the dull routine of everyday life.
  某物可以使某人的生活更加有乐趣,也就是说,某物为每天枯燥的生活增添了一些色彩
2.sth have become the foucus of one's life and the source of one's happiness and contentment.
  某物已成为某人生活的中心和快乐的来源

能力

1.equip sb with the ability to do sth.
  ★使某人拥有做某事的能力
2.be competent enough to do sth.
  ★有能力做某事
3.tell the useful information from the useless one.
  ★从无用信息中挑出有用信息
4.sth will provide sb with more opportunities to develop one’s interpersonal skills, which may put them in a favorable position in the future job markets.
  某事会使某人有更多机会发展人际交往能力,而这对他(们)未来找工作是非常有好处的
5.By doing sth, sb can not only improve one’s academic studies, but gain many abilities which he will never be able to get from the textbooks.
  通过做某事,某人不仅能够提高他们的专业能力,而且能获得从课本上得不到的能力
6.tap one's potential.
  ★开发潜能
7.sth will help people foster one's independence.
  某物帮助某人培养独立
8.help strengthen one's sense of responsibility.
  ★帮助加强某人的责任感
9.achieve one's goal.
  ★达成某人的目标
10.learn from mistake.
  ★吸取教训
11.surmount difficulities
  ★战胜困难

交流

1.develop and cultivate character and interpersonal skills.
  ★培养性格和人际交往的能力
2.make eye contact.
  ★做眼神交流
3.live only in the virtual world.
  ★仅仅生活在虚拟世界
4.hinder communication.
  ★阻碍沟通
5.The over-dependence on mobile phones can harm the relationship among friends and family.
  过度依赖手机会伤害朋友以及家人的关系
6.At dinner tables, instead of chatting and laughing with each other, many people choose to chat with other friends online.
  在饭桌前,人们不是相互聊天谈笑,而是选择和朋友在网上聊天
7.Many people have become over-dependent on the Internet and neglected face-to-face communication.
  很多人变得太依赖网络并且忽略了面对面交流。
8.The Internet enables more effective communication in some situations, but over-dependence on it actually pulls people apart.
  网络在一些情况下使交流更有效,但是过度依赖它实际是人们更疏远

知识

1.Little by little, our knowledge will be well enriched, and our horizons will be greatly broadened.
  逐渐的,我们的知识会得到增加,我们的视野将会大大的拓宽
2.reduce study load 
  ★减轻学习负担
3.sth can broaden people's minds to the extent which may not be reached previously.
  某物可以扩大人们的视野到一个之前不能到达的程度

方便

1.sth can save sb a great deal of time, so sb would be able to concentrate more time and energy on one's academic work.
  某物能节省大量时间,这会使某人有更多的时间和精力放在学习上
2.sth have brought people great convenience in doing sth.
  某物在人们做某事时带来了巨大的方便
3.There is no denying that sth have greatly enhanced work/study efficiency.
  毋庸置疑,某物很大的提升了工作/学习效率

经济

1.relieve one's families' financial burden to some extent.
  这在一定基础上可以缓解某人家庭的经济负担
2.The welfare and salary have been improved greatly.
  福利和收入得到了巨大的提升
3.Money is so indispensable in people’s lives that without it no material comforts or well-being can be guaranteed.
  钱在生活中必不可少,没有了钱,也就谈不上物质享受和幸福了
4.Money doesn't necessarily ensure happiness or well-being. 
  钱不一定能保证幸福
5.can afford the huge cost of sth.
  ★能够承担某物的高额
6.bring huge economic loss to sb.
  ★给某人带来巨大经济损失

安全

1.sth pose a great threat to one's safety.
  某物对某人的安全造成巨大威胁
2.ensure the safety of sb. 
  ★确保某人的安全
3.sth add to the safety of sth.
  某物增加了某事的安全
4.food safety.
  ★食品安全

环境

1.To balance economic growth with environmental protection is highly important.
  权衡经济发展和环境保护是高度重要的
2.We all should bear in mind that environmental protection is everybody's duty.
  我们都应该记住环境保护使每个人的职责
3.sth help to build harmonious cooperation and effective communication, which helps to create a pleasant working atmosphere.
  某事帮助建立和谐的合作关系和有效的沟通,这将帮助建立一个愉快的工作氛围
阅读全文

SpringCloud基础+电商实战

2024/4/11

Spring Cloud基础介绍

  • 微服务的基本概念、设计与拆分原则
  • 微服务和Spring Cloud的关系
  • 微服务常见的组件和功能
  • 课程查询案例基本介绍、系统架构设计和接口设计
  • 分模块构建Spring Cloud项目
  • 完成课程列表、课程价格服务开发
  • 课程服务整合,服务注册与发现
  • 整合Feign实现服务间调用
  • 网关的集成与开发,并接入服务
  • 引入服务的熔断与降级,并进行实操演练

微服务基础

  • 什么是微服务?
  • 微服务的特点
  • 微服务优缺点
  • 微服务的两大门派 [Spring Cloud 和 Dubbo]
  • 微服务拆分
  • 微服务扩展
  • 微服务重要模块

微服务热度

单体应用的痛点

什么是服务化
  • 把传统的单机应用中的本地方法调用,改造成通过RPC、HTTP产生的远程方法调用
  • 把模块从单体应用中拆分出来,独立成一个服务部署
什么是微服务
  • 一系列、一部分

  • 是一种架构风格

  • 开发单体应用作为一系列小型服务的套件,其中每个服务都运行再自己的进程中,并且通过轻量级的机制实现彼此间的通信,这通常是HTTP资源API

  • 这些服务是围绕着业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署

  • 这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术

微服务的特点

  • 组件以服务形式来提供

  • 产品不是项目

  • 轻量级通信、独立进程

  • 分散治理、去中心化治理

  • 容错性设计

  • 会带来团队组织架构的调整

微服务优缺点

  • 服务简单、便于学习和上手,相对易于维护
  • 独立部署,灵活扩展
  • 技术栈丰富
微服务缺点
  • 运维成本过高 [磁盘满 CPU高]
  • 接口可能不匹配
  • 代码可能重复 [要非常明确的定义每个API]
  • 架构复杂度提高

微服务两大门派

  • Spring Cloud:众多子项目【最大供应者:Netflix内容付费知识】
  • dubbo:高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
[Dubbo虽然有很多不提供的组件 但是可以和其他供应商合作完成提供任务]
核心组件 Dubbo Spring Cloud
服务注册中心 Zookeeper Spring Cloud Netflix Eureka
服务调用方式 RPC REST API
服务网关 Spring Cloud Netflix Zuul
断路器 不完善 Spring Cloud Netflix Hystrix
分布式配置 Spring Cloud Config
服务跟踪 Spring Cloud Sleuth
消息总线 Spring Cloud Bus
数据流 Spring Cloud Stream
批量任务 Spring Cloud Task
通信协议对比
  • RPC vs REST
  • 服务提供方与调用方接口依赖方式太强
  • 服务对平台敏感,难以简单复用
文档质量对比
  • Dubbo的文档可以说再国内开源框架中算是一流的,提供了中文与英文两种版本
  • Spring Cloud文档体量大,更多的是偏向整合,更深的使用方法还是需要查看整合组件的详细文档
两大门派选型建议
  • Dubbo => 组装电脑 【中文文档多】
  • Spring Cloud => 品牌机【稳定可靠】

微服务拆分

什么时候进行服务化拆分
  • 第一阶段的主要目标是快速开发和验证想法
  • 进一步增加更多的新特性来吸引更多的目标用户
  • 同时进行开发的人员超过10人,这个时候就该考虑到服务化拆分了
不适合拆分的情况
  • 小团队,技术基础较薄弱
  • 流量不高,压力小,业务变化也不大
  • 对延迟很敏感的低延迟高并发系统
服务化拆分的两种方式
  • 纵向拆分[上方区别图]
  • 横向拆分
  • 结合业务综合分析

服务扩展

维度

自动按需扩展
  • 根据CPU负载程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来决定是否扩展
  • 自动分配一个新的服务实例,提高可用性
  • 提高了可伸缩性(双11之后,自动减少服务器)
  • 具有最佳使用率,节约成本

微服务重要模块

  • 服务描述
  • 注册中心
  • 服务框架
  • 负载均衡
  • 熔断和降级
  • 网关

Spring Cloud课程查询

  • Spring Cloud简介
  • 项目整体设计
  • 课程列表模块开发
  • 课程价格模块开发 [模块间互相调用]
  • 服务注册与发现Eureka
  • 服务间调用Feign
  • 负载均衡Ribbon
  • 熔断器Hystrix [兜底界面 默认返回]
  • 网关Zuul
  • 整体测试

Spring Cloud简介

  • 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
核心组件
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用方式 REST API、Feign、Ribbon
服务网关 Spring Cloud Netflix Zuul
熔断器 Spring Cloud Netflix Hystrix

项目整体设计

  • 项目介绍

  • 接口设计
    • 课程列表
    • 单个课程价格
    • 整合课程列表和价格
  • 表设计

系统数据流向

【课程列表数据】→ 【课程列表服务】

【课程价格数据】→ 【课程价格服务】→ 【整体列表+价格】

新建多模块项目

创建一个 spring-cloud-course-pracice 并删除src文件夹
新建New → module → Maven → course-service → 删除src
    新建New → module → New Module → Maven → course-list → Parent:course-service 
    新建New → module → New Module → Maven → course-price → Parent:course-service 
D:\Java+4399\阶段5:Java分布与微服务实战\第32周 Spring Cloud基础\第2节 Spring Cloud开发课程查询功能\辅助材料\SpringCloud课程查询源码【优质it资源微信it-wangke18】.zip\SpringCloud课程查询源码\课程价格模块开发后\spring-cloud-course-practice
course-service.course-list
pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-list</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CourseListController.java
package com.imooc.course.controller;

import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * CourseListController课程列表Controller
 */

@RestController
public class CourseListController {
    //提供课程列表服务
    @Autowired //引入课程服务
    CourseListService courseListService;


    @GetMapping("/courses")
    public List<Course> courseList() {
        //return → service
        return courseListService.getCourseList();
    }
}
com/imooc/course/service/CourseListService.java
package com.imooc.course.service;

import com.imooc.course.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/course/service/impl/CourseListServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CourseMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 课程服务实现类
 */
@Service
public class CourseListServiceImpl implements CourseListService {
    @Autowired
    CourseMapper courseMapper;
    @Override
    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/course/entity/Course.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * Course的实体类
 */
public class Course implements Serializable {
    Integer id;
    Integer courseId;
    String courseName;
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", courseName='" + courseName + '\'' +
                ", valid=" + valid +
                '}';
    }
    Getter+Setter  
}
com/imooc/course/dao/CourseMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 描述课程的Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FROM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity的实体类与数据库名进行驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=====================================================================
数据库名:course_practice
   表名:course
   字段:id[自增]   course_id   course_name   valid
课程列表模块开发-总结 [注意点]
  • 多模块开发
  • 实体类实现Serializable接口、set方法
  • MyBatis的驼峰配置
course-list.course-price
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-price</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 课程价格控制器
 */

@RestController
public class CoursePriceController {
    @Autowired
    CoursePriceService coursePriceService;
    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId){
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }
}
com/imooc/course/service/CoursePriceService.java
package com.imooc.course.service;

import com.imooc.course.entity.CoursePrice;

import java.util.List;

/**
 * 课程价格服务
 */
public interface CoursePriceService {
    CoursePrice getCoursePrice(Integer courseId);
}
com/imooc/course/service/impl/CoursepriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 课程价格的服务实现类
 */
@Service
public class CoursepriceServiceImpl implements CoursePriceService {
    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }
}
com/imooc/course/entity/CoursePrice.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * CoursePrice的实体类
 */
public class CoursePrice implements Serializable {
    Integer id;
    Integer courseId;
    Integer price;

    @Override
    public String toString() {
        return "CoursePrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", price=" + price +
                '}';
    }
    Getter+Setter
}
com/imooc/course/dao/CoursePriceMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 课程价格Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {
    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrice(Integer courseId);
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
==========================================================
http://127.0.0.1:8082/price?courseId=409
数据库名:course_practice
   表名:course_price
   字段:id[自增]   course_id    price

Eureka的作用和架构

Eureka
  • 用于定位服务,直接找到组件中的各个服务地址
  • 114[各种服务的提供者]、物业[维护各个住户的信息(注册中心)]
为什么需要服务注册与发现 [移除不影响 但会有很多麻烦]
  • IP变化
  • 难以维护
  • 改进
    • 节点变化[服务的提供者和消费者] [消费者需要调用提供者的API来获得服务] 若提供者修改了ip 此时应该上传到注册中心,消费者此时需要得到IP和API 无需直接找提供者 直接去注册中心调用
Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

引入Eureka

  • 引入依赖
  • 配置文件
  • 启动注解
pom.xml(eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-course-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>course-eureka-server</name>
    <description>Spring Cloud Eureka</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml(Spring-cloud-course-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>pom</packaging>
  <modules>
    <module>course-service</module>
    <module>eureka-server</module>
  </modules>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.imooc</groupId>
  <artifactId>spring-cloud-course-practice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-cloud-course-practice</name>
  <description>course project for Spring Cloud</description>

  <properties>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!--  表示Spring Cloud的版本-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
application.properties(eureka-server)

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#fetch-registry???????????????????
eureka.client.fetch-registry=false
#register-with-eureka??????????Eureka Server????true?
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application.properties(course-list)

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity?????????????????
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
#??????? eureka-server??application.properties?defaultZone????????????????
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
application.properties(course-price)

server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-price
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/EurekaServerApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka的服务端
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
========================================================
http://127.0.0.1:8000/

利用Feign实现服务间调用

Feign
  • 声明式、模板化的HTTP客户端,方便的调用远程的HTTP请求 [基于接口实现]
集成Feign
  • 引入依赖
  • 配置文件
  • 注解
pom.xml (course-price)
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

application.properties
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import java.util.List;
import javax.xml.ws.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:     课程价格控制器
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;

    @Autowired
    CourseListClient courseListClient;

    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }

    @GetMapping("/coursesInPrice")
    public List<Course> getCourseListInPrice(Integer courseId) {
        List<Course> courses = courseListClient.courseList();
        return courses;
    }
}
com/imooc/course/client/CourseListClient.java[接口]
package com.imooc.course.client;

import com.imooc.course.entity.Course;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 描述:     课程列表的Feign客户端
 */
@FeignClient("course-list")
public interface CourseListClient {

    @GetMapping("/courses")
    List<Course> courseList();
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=============================================================
http://127.0.0.1:8082/coursesInPrice

[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]

负载均衡的两种类型

  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡策略
  • RandomRule 表示随机策略
  • RoundRobinRule 表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
配置不同的负载均衡方式
  • Ribbon.NFLoadBalancerRuleClassName
application.properties(course-price)
course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

为什么要断路器

Hystrix
pom.xml(course-price)   
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
application.properties(course-price)
feign.hystrix.enabled=true
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
================================================================
所有服务都打开时 http://127.0.0.1:8082/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud白学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
当把CouresListApplication服务关闭的时候
[
    {
        "id": 1,
        "courseId": 1,
        "courseName": "默认课程",
        "valid": 1
    }
]

整合两个服务

com/imooc/course/controller/CoursePriceController.java
    @GetMapping("/coursesAndPrice")
    public List<CourseAndPrice> getCoursesAndPrice(){
        List<CourseAndPrice> courseAndPrices = coursePriceService.getCourseAndPrice();
        return courseAndPrices;
    }
=====================================================
http://127.0.0.1:8082/coursesAndPrice
[
    {
        "id": 1,
        "courseId": 362,
        "name": "SpringCloud自学",
        "price": 348
    },
    {
        "id": 2,
        "courseId": 409,
        "name": "玩转Java并发工具",
        "price": 399
    }
]
com/imooc/course/service/CoursePriceService.java
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);
    List<CourseAndPrice> getCourseAndPrice();
}
com/imooc/course/service/impl/CoursePriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CourseAndPrice;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:     课程价格的服务实现类
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Autowired
    CourseListClient courseListClient;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }

    @Override
    public List<CourseAndPrice> getCourseAndPrice() {
        List<CourseAndPrice> courseAndPrices = new ArrayList<>();
        List<Course> courses = courseListClient.courseList();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            //inn
            if (course != null) {
                CoursePrice coursePrice = getCoursePrice(course.getCourseId());
                CourseAndPrice courseAndPrice = new CourseAndPrice();
                courseAndPrice.setPrice(coursePrice.getPrice());
                courseAndPrice.setName(course.getCourseName());
                courseAndPrice.setId(coursePrice.getId());
                courseAndPrice.setCourseId(course.getCourseId());
                courseAndPrices.add(courseAndPrice);
            }
        }
        return courseAndPrices;
    }
}
com/imooc/course/entity/CourseAndPrice.java
package com.imooc.course.entity;

/**
 * 课程与价格的融合类
 */
public class CourseAndPrice {
    Integer id;
    Integer courseId;
    String name;
    Integer price;
}

网关Zuul

  • 为什么需要网关
  • 签名校验、登录校验冗余问题
  • Spring Cloud Zuul 与 Spring Cloud
  • API网关允许您将API请求(内部或外部)路由到正确的位置

集成Zuul [统一修改访问url地址]

  • 把自己注册到Eureka这个注册中心
  • 引入依赖
  • 配置路由地址
pom.xml(course-zuul)
     <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>
com/imooc/course/ZuulGatewayApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;

/**
 * 网关启动类
 */
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
===============================================================
http://127.0.0.1:9000/imooc/price/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
application.properties

spring.application.name=course-gateway
server.port=9000
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

zuul.prefix=/imooc
zuul.routes.course-list.path=/list/**
zuul.routes.course-list.service-id=course-list
zuul.routes.course-price.path=/price/**
zuul.routes.course-price.service-id=course-price

利用网关实现过滤器

  • pre 过滤器在路由请求之前运行
  • route 过滤器可以处理请求的实际路由
  • post 路由请求后运行过滤器
  • error 如果在处理请求的过程中发生错误,则过滤器将运行
com/imooc/course/filter/PreRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 记录请求时间
 */
@Component
public class PreRequestFilter extends ZuulFilter {

    @Override
    public String filterType() {
        //过滤器的类型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否启用过滤器
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //通过时间戳 获取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        currentContext.set("startTime",System.currentTimeMillis());
        System.out.println("过滤器已经记录时间");
        return null;
    }
}
com/imooc/course/filter/PostRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 请求处理后的过滤器
 */
@Component //有了才能被spring捕捉到
public class PostRequestFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() { //在其之前-1 先运行
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        Long startTime = (Long) currentContext.get("startTime");
        long duration = System.currentTimeMillis() - startTime;
        String requestURI = currentContext.getRequest().getRequestURI();
        System.out.println("uri:" + requestURI + ",处理时长:" + duration);
        return null;
    }
}


Spring Cloud电商实践

  • 服务拆分过程分析和经验分享
  • 完成用户、商品、购物车和订单等服务开发
  • 通用common模块的拆分和应用
  • Eureka server注册中心开发,用Feign完成服务之间的调用
  • 共享Session的处理方案
  • 统一网关的集成与开发

Spring Cloud电商项目

  • 项目介绍
    • 在Spring Boot的基础上升级为Spring Cloud
    • 从0到1 (Spring Boot)
    • 从1到多、微服务
  • 模块拆分
  • Eureka-server开发
  • 用户模块开发
  • 公共模块开发
  • 网关模块开发
  • 商品分类和商品模块开发
  • 购物车和订单模块开发
  • 总结

模块拆分

  • 粒度:过粗、适中、过细

  • 人员的角度

  • 业务的角度:相关、独立

  • Eureka-server模块

  • 网关模块

  • 公共模块 (md5、统一API返回、常量、异常、异常枚举、二维码、工具类模块……)

  • 用户模块 (相对来说比较独立,登录的时候无需获得商品信息)

  • 商品分类和商品模块

  • 购物车和订单模块

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

``

项目初始化

Eureka-server模块开发
  • 引入依赖
  • 配置文件
  • 启动注解

由于是多模块开发,创建了maven项目后把cloud-mall-practice的src删除

pom.xml(cloud-mall-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>cloud-mall-practice</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>cloud-mall-eureka-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
cloud-mall-eureka-server/com/imooc/cloud/mall/practice/eureka/EurekaServerApplication.java
package com.imooc.cloud.mall.practice.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * 1. Eureka Server的启动类,提供服务注册与发现
 * 2写其pom和resources的配置文件
 *
 */
@EnableEurekaServer //对外提供服务
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
pom.xml(cloud-mall-eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <name>
        cloud-mall-eureka-server
    </name>
    <description>Spring cloud Eureka Server</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#是否同步其他节点的信息
eureka.client.fetch-registry=false
#是否把自己作为服务注册在服务上
eureka.client.register-with-eureka=false
#eureka-server所在的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

用户模块知识点

登录、注册[加盐md5]、重名校验[注册就不可注册]、密码加密存储、Session的使用、越权校验[不可编辑别人的签名]

用户模块
  • 表设计
  • 开发
  • 测试

idea中如何将包名折叠或者或如何将折叠的包名展开_idea包名折叠-CSDN博客

用户模块初始化

公共模块
  • 常量、异常、工具类
  • 自身不是Spring Boot项目
进行模块各层级的重构 + 用户模块的测试
一定要记得如果引用其他muder的时候 要在xml中假如其项目文件的依赖才可以跨项目引用
比如我这个项目是【一定要引用噢!! 不然在非其项目的时候找不到import导包】
pom.xml(cloud-mall-zuul)
           <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

网关模块开发

pom.xml(cloud-mall-zuul)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-zuul</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties(cloud-mall-zuul)
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
com/imooc/cloud/mall/practice/zuul/filter/UserFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 用户鉴权过滤器
 */
@Component
public class UserFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("images") || requestURI.contains("pay")) {
            return false;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/filter/AdminFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import com.imooc.cloud.mall.practice.zuul.feign.UserFeignClient;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 管理员鉴权过滤器
 */
@Component
public class AdminFilter extends ZuulFilter {
    @Autowired
    UserFeignClient userFeignClient;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("adminLogin")){
            return false;
        }
        if (requestURI.contains("admin")){
            return true;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10010,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
            return null; //程序可以停止了
        }
        //进一步判断是否是管理员
        Boolean adminRole = userFeignClient.checkAdminRole(currentUser);
        if (!adminRole){
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10011,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.zuul.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * UserFeignClient
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    @PostMapping("/checkAdminRole")
    public Boolean checkAdminRole(@RequestBody User user);
}
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
网关模块加上SpringBoot启动类

package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
=================================================================
127.0.0.1:8083/user/login?userName=mumu9&password=lihao123
{
    "status": 10000,
    "msg": "SUCCESS",
    "data": {
        "id": 21,
        "username": "mumu9",
        "password": null,
        "personalizedSignature": "乘风破浪的姐姐",
        "role": 2,
        "createTime": "2024-04-17T18:08:15.000+0000",
        "updateTime": "2024-04-17T18:10:49.000+0000"
    }
}

POSTMAN中cloud-mall-practice的注册新用户、用户登录、管理员登录、登出模块都通过网关共用地址的调用,但是更新个性签名不可以,提示需要登录,因为更新签名需要提前登录。但是问题的最主要的点就是没有拿到session所以并没有把用户信息传到个性签名中 这是就需要Session共享机制

Session共享机制

登录功能分析
  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,先从session中获取用户信息,然后再执行业务逻辑
目前遇到的障碍 [记得去电脑端启动Redis]
  • session被网关过滤
  • 共享session [现在是多模块项目 其他项目保存了session 另一个项目无法获得]
  • EnableRedisHttpSession[需要通过中介Redis去调取]
在cloud-mall-user/application.properties中
server.port=8081

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-user

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
在cloud-mall-zuul/application.properties中
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=

#没有敏感的需要过滤
zuul.sensitive-headers=
zuul.host.connect-timeout-millis=15000
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
之后在UserApplication和ZuulGatewayApplication前面加上@EnableRedisHttpSession

com/imooc/cloud/mall/practice/user/UserApplication.java
package com.imooc.cloud.mall.practice.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 启动类
 */
@SpringBootApplication
@EnableSwagger2
@MapperScan(basePackages = "com.imooc.cloud.mall.practice.user.model.dao")
@EnableRedisHttpSession
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

-------------------------------------------------------------------------------------

com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}

商品分类与商品模块初始化

什么是商品分类
  • 条理清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对于后台管理人员不友好,不方便管理

[cloud-mall-category-product]创造出来,将controller,model(dao,pojo),service,impi 里转入 Category来搞 因为在网关项目里写了关于管理员校验的方法 所以在此次处CategoryController把session里的校验删除

要让商品和商品目录用到User类 一定要去pom文件里添加依赖 不然idea找不到
pom.xml(cloud-mall-category-product)
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
com/imooc/cloud/mall/practice/categoryproduct/config/CachingConfig.java
创建处理缓存的配置类 configpackage com.imooc.cloud.mall.practice.categoryproduct.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

*/**
* ** 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口
*   CategoryVO implements Serializable
* **/
*@Configuration
@EnableCaching
public class  CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
在网关中增加path和service-id
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user

zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
查端口  => netstat -ano | findstr :8083 [pid为7812]
杀死端口PID => taskkill /PID 7812 /f

商品模块

更新和新增商品
  • 合并写法不可取
  • 业务逻辑清晰、独立
批量上下架
  • MyBatis遍历List
  • where语句拼接
商品列表:搜索功能
入参判空 → 加%通配符 → like关键字
对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 这里要拿到某一个目录Id下的所有子目录id的List

前台:商品列表

  • 排序功能
  • Mybatis PageHelper
  • 枚举:order by
把所有product的java都移动过去 controller/service/impl/modal.dao/modal.pojo/query.ProductListQuery/request.AddProductReq、ProductListReq、UpdateProductReq/resourcs.mappers.ProductMapper.xml
将Constant里的@Value("${file.upload.dir}")的文件上传目录重构到category-product项目中
然后在其项目中的resources的application.properties中写一下路径
文件地址映射:config/imoocMallWebMvcConfig

图片端口的特殊处理

application.properties(cloud-mall-category-product)
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/imooc-mall-prepare-static/

不能再通过获取getHost来配置了 根据实际情况去配置 没有办法获取到网关对外暴露的真正端口号
 private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }

要改为com/imooc/cloud/mall/practice/categoryproduct/controller/ProductAdminController.java
/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq  59.需要ProductService.java
 */
@RestController
public class ProductAdminController {

    @Autowired
    ProductService productService;

    @Value("${file.upload.ip}")
    String ip;

    @Value("${file.upload.port}")
    Integer port;

    @PostMapping("/admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }

    @PostMapping("/admin/upload/file")
    public ApiRestResponse upload(HttpServletRequest httpServletRequest,
                                  @RequestParam("file") MultipartFile file) {
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件
        File fileDirectory = new File(ProductConstant.FILE_UPLOAD_DIR);
        File destFile = new File(ProductConstant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()) {
            if (!fileDirectory.mkdir()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            return ApiRestResponse
                    .success(getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/category-product/images/"
                            + newFileName);
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }

    private URI getHost(URI uri) {
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), ip, port,
                    null, null, null);
        } catch (URISyntaxException e) {
            effectiveURI = null;
        }
        return effectiveURI;
    }

阶段性重难点和常见错误

阶段总结
  • 重难点:模块拆分设计、公共模块、Zuul(网关)过滤器、Session处理、Feign调用
    Session在微服务的情况下就不容易处理到了可以共享Session,把其放在Redis中实现共享
    Feign模块之间的接口调用[HTTP是手动调用]
  • 常见错误:模块粒度不合适、无公共模块、各接口独立校验、session无法共享、HTTP手动调用
模块拆分
  • 粒度:过粗、适中、过细
  • 人员的角度
  • 业务的角度:相关、独立

购物车与订单模块

购物车模块
  • 添加商品到购物车 → 商品是否在售、是否有库存

    • →[否] 提示用户

    • →[是] 该商品之前就在购物车里

      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
创建一个module 加入关于cart的controller/model.dao.pojo.vo/service.impl
其中ProductMapper productMapper会爆红是理所应当的 这样证明耦合不是很严重
利用远程调用Feign进行调用
思路:通过调查发现productMapper只用于挑选ID
Product product = productMapper.selectByPrimaryKey(productId);
所以可以去重构一下代码 回到商品模块的地方cloud-mall-category-product
其中controller中的ProductController
//com/imooc/cloud/mall/practice/categoryproduct/controller/ProductController.java
//这个是服务与服务之间的内部调用 不需要层层包装 只需要返回就好
    @GetMapping("product/detailForFeign")
    public Product detailForFeign(@RequestParam Integer id){
       Product product = productService.detail(id);
       return product;
    }
//千万不要直接引用另一个项目的mapper 因为耦合有点严重 只要对面发生变化 就完蛋了
//com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.categoryproduct.model.pojo.Product;
import org.springframework.web.bind.annotation.RequestParam;

public interface ProductFeignClient {
    Product detailForFeign(@RequestParam Integer id);
}

用户模块提供获取当前用户接口

com/imooc/cloud/mall/practice/user/controller/UserController.java
 @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    } /**
     * 获取当前登录的User对象 为了避免暴露用户信息,可以再filter中对getUser进行拦截
     * @param session
     * @return
     */
    @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    }
com/imooc/cloud/mall/practice/cartorder/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * UserFeign客户端
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    /**
     * 获取当前登录的user对象
     */
    @GetMapping("/getUser")
    User getUser();
}
修改CaetMapper.xml的路径名
com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.cartorder.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 描述:     商品FeignClient
 */
@FeignClient(value = "cloud-mall-category-product")
public interface ProductFeignClient {
    //不经过网关 只是内部调用
    @GetMapping("product/detailForFeign")
    Product detailForFeign(@RequestParam Integer id);

    @PostMapping("product/updateStock")
    void updateStock(@RequestParam Integer productId, @RequestParam Integer stock);

}
将新增模块加入到网关[前缀地址] zuul
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
zuul.routes.cloud-mall-cart-order.path=/cart-order/**
zuul.routes.cloud-mall-cart-order.service-id=cloud-mall-cart-order

让Feign携带Session信息

错误新消息:500 Internal Server Error
在购物车中去调用User的getUser方法 是经过Feign
但是Feign的调用不经过网关 它是一个HTTP的调用
需要携带网关的Session信息

  • FeignRequestInterceptor [对每一个发出的Feign进行拦截]
    把网关的所有信息都复制到Feign的请求上 就不会遗漏相关信息
com/imooc/cloud/mall/practice/cartorder/filter/FeignRequestInterceptor.java
package com.imooc.cloud.mall.practice.cartorder.filter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 描述:     Feign请求拦截器
 */
@EnableFeignClients
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //通过RequestContextHolder获取到请求 拿到requestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return;
        }//类型转换 拿到Request
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        Enumeration<String> headerNames = request.getHeaderNames(); //拿到所有Header名字
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) { //如果还有新的元素 先获取
                String name = headerNames.nextElement(); //获取相关的值
                Enumeration<String> values = request.getHeaders(name);
                while (values.hasMoreElements()) {//获取到当前元素
                    String value = values.nextElement();
                    requestTemplate.header(name, value);
                }
            }
        }
    }
}

订单模块

  • 表设计和接口设计
"订单编号的名字也要改 不能是以前的自增id 要变成order_no"
order_no是每一种商品的id
否则黑客早上下一单 晚上下一单 相减 就可以估计客流量 
分成了很详细的字段名
product_id/name/img 都是之前购买过的订单产生的数据

生成订单–用户下单

  • 入参
  • 从购物车中查询已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖
  • 调用商品服务扣库存 [及时更新库存]
  • 删除购物车为中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每一个商品到order_item表

图片路径配置

com/imooc/cloud/mall/practice/cartorder/config/ImoocMallWebMvcConfig.java
package com.imooc.cloud.mall.practice.cartorder.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 描述:     地址映射配置
 */
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    @Value("${file.upload.dir}")
    String FILE_UPLOAD_DIR;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + FILE_UPLOAD_DIR);
    }

购物车和订单模块-重难点

  • 订单表[多个关联(订单编号+状态)]、订单状态设计
  • 购物车流程
  • 下单流程
  • Feign调用的处理
购物车和订单模块-常见错误
  • Feign调用取不到User对象
  • URL错误拦截(图片url可以不拦截[订单+购物车])
  • 路由配置错误(二维码/图片不显示)
阅读全文

多线程与分布式

2024/4/4

多线程与分布式 [周介绍]

  • 线程池的基本使用、特点、注意点
  • ThreadLoacal的基本使用、原理和注意事项
  • 分布式基础、核心概念
  • docker的下载、安装和基本命令
  • 独立制作docker容器
  • Nginx的安装、基本使用和使用命令
  • 使用Nginx搭建文件服务
  • 消息队列RabbitMQ的核心概念queue、message和exchange
  • RabbitMQ的四种交换机模式
  • SpringBoot整合RabbitMQ案例

线程池 —— 治理线程的法宝

  • 线程池的自我介绍
  • 创建和停止线程池
  • 常见线程池的特点和用法
  • 任务太多,怎么拒绝
  • 钩子方法,给线程池加点料
  • 实现原理、源码分析
  • 使用线程池的注意点
线程池的自我介绍
  • 线程池的重要性 [可以复用我们的线程]
  • 什么是”池” – 软件中的”池”,可以理解为计划经济
  • 如果不适用线程池,每个任务都新开一个线程处理
    • 一个线程
    • for循环创建线程
    • 当任务数量上升到1000
      这样开销太大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题[多了会报错 内存不足异常]
threadpool/ForLoop.java
package threadpool;
/**
 * 描述:     TODO
 */
public class ForLoop {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Task());
            thread.start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("执行了任务");
        }
    }
}
为什么要使用线程池
  • 问题一:反复创建线程开销大
  • 问题二:过多的线程会占用太多内存
  • 解决以上两个问题的思路
    • 用少量的线程——避免内存占用过多
    • 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗
线程池的好处
  • 加快响应速度
  • 合理利用CPU和内存
  • 统一管理
线程池使用应用的场合
  • 服务器(Tomcat)接收到大量请求时,使用线程池技术是非常合适的,它可以大大减少现成的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

线程增减的时机

创建和停止线程池
  • 线程池构造方法的参数
  • 线程池应该手动创建还是自动创建
  • 线程池里的线程数量设定为多少比较合适
  • 停止线程池的方法
线程池构造方法的参数
参数名 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
keepAliveTime long 保持存活时间
workQueue BlockingQueue 任务存储队列
threadFactory ThreadFactory 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
Handler RejectedExecutionHandler 由于线程池无法接收你所提交的任务的拒绝策略
参数中的corePoolSize和maxPoolSize
  • corePoolSize指的是核心线程数
    线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新的线程去执行任务

  • 最大量maxPoolSize
    在核心线程数的基础上,格外增加的线程数的上限

添加线程规则
  1. 如果线程数小于corePoolSize,创建一个新线程来运行新任务
  2. 如果线程数等于(或大于) corePoolSize但少于maximumPoolSize,则将任务放入队列
  3. 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程
  4. 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝

  • 是否需要增加线程的判断顺序是:
    • corePoolSize
    • workQueue
    • maxPoolSize
举个例子
  • 线程池:核心池大小为5,最大池大小为10,队列为100
  • 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建新的线程maxPoolSize,最多到10个线程,如果再来任务,就拒绝
增减线程的特点
  • 通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
  • 线程池希望保持较小的线程数,并且只有在负载变得很大时才增加它
  • 通过设置maximumPoolSize为很高的值,可以允许线程池容纳任意数量的并发任务
  • 只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize

线程存活时间和工作队列

keepAliveTime
  • 如果线程池当前的线程多余corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,它们就会被终止
ThreadFactory 用来创建线程
  • 默认使用Executors.defaultThreadFactory()
  • 创建出来的线程都在同一个线程组
  • 如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等
工作队列
  • 有三种最常见的队列类型
    • 直接交接:SynchronousQueue
    • 无界队列:LinkedBlockingQueue
    • 有界队列:ArrayBlockingQueue

自动创建线程池的风险

线程池应该手动创建还是自动创建
  • 手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险
    自动创建线程池(即直接调用JDK封装好的构造方法) 可能会带来哪些问题?
  • newFixedThreadPool
    • 容易造成大量内存占用,可能会导致OOM
threadpool/FixedThreadPoolThread.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolThread {
    public static void main(String[] args) { //核心线程数量
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
class Task implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName());
    }
}
=======================================================================
pool-1-thread-3
pool-1-thread-4
pool-1-thread-1
pool-1-thread-2
    
pool-1-thread-3
pool-1-thread-1
pool-1-thread-4
pool-1-thread-2
因为核心线程数量只规定了4个 这有这四个线程跑程序
threadpool/FixedThreadPoolOOM.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示nexFixedThreadPool出错的情况
 */
public class FixedThreadPoolOOM {
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++){
            executorService.execute(new SubThread());
        }
    }
}
class SubThread implements Runnable{

    @Override
    public void run() {
        try{//一直睡觉 不让它结束
            Thread.sleep(1000000000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
=======================================================================
报错:java.lang.OutOfMemoryError:GC overhead limilt exceeded
  • newSingleThreadExecutor [单独的线程]
    • 当请求堆积的时候,可能会占用大量的内存
threadpool/SingleThreadExecutor.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
=======================================================================
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
  • CachedThreadPool [可缓存线程池]
    • 特点:具有自动回收多余线程的功能
    • 弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE, 这可能会创建数量非常多的线程,甚至导致OOM
threadpool/CachedThreadPool.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
  • newScheduledThreadPool [跟时间相关的]
threadpool/ScheduledThreadPool.java
package threadpool;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
        threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);//每隔3s运行
    }
}
  • 正确的创建线程池的方法
    • 根据不同的业务场景,设置线程池参数
    • 比如:内存有多大,给线程取什么名字等等

线程池里的线程数量设定为多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍
    参考Brain Goetz推荐的计算方法:
    线程数 = CPU核心数 × (1 + 平均等待时间 / 平均工作时间)

对比各种线程池的特点

常见的线程池的特点

以上4种线程池的构造方法的参数
Parameter FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreaded
corePoolSize constructor-arg 0 constructor-arg 1
maxPoolSize same as corePoolSize Integer.MAX_VALUE Integer.MAX_VALUE 1
keepAliveTime 0 seconds 60 seconds 0 seconds 0 seconds

阻塞队列分析

  • FixedThreadPool和SingleThreadExecutor的Queue是LinedBlockingQueue
  • CachedThreadPool使用的是Queue是SynchronousQueue
  • ScheduledThreadPool使用延迟队列DelayedWorkQueue
workStealingPool是JDK1.8加入的
  • 这个线程池和之前的都有很大不同
  • 子任务
  • 窃取 [并行执行]

如何正确关闭线程池

停止线程池的正确方法
  • shutdown [再给就拒绝 新的任务不会增加了]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        executorService.execute(new ShutDownTask());
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

=================================================================================
pool-1-thread-4
pool-1-thread-9
pool-1-thread-10
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task threadpool.ShutDownTask@61bbe9ba rejected from java.util.concurrent.ThreadPoolExecutor@610455d6[Shutting down, pool size = 10, active threads = 10, queued tasks = 970, completed tasks = 20]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
    at threadpool.Shutdown.main(Shutdown.java:18)
pool-1-thread-8
pool-1-thread-1
pool-1-thread-3
  • isShutdown [判断是否进入停止状态]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());//false
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        System.out.println(executorService.isShutdown());//true 已经结束了
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • isTerminated [线程停止返回 整个程序执行完毕]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());//false
        executorService.shutdown();//绅士的暂停 再去提交的任务就不会增加了
        System.out.println(executorService.isShutdown());//true 已经结束了
        System.out.println(executorService.isTerminated());
        Thread.sleep(10000);
        System.out.println(executorService.isTerminated());
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
==========================================================================
pool-1-thread-5
pool-1-thread-10
false
true
false
pool-1-thread-2
pool-1-thread-6
pool-1-thread-6
pool-1-thread-2
true
  • awaitTermination [测试一段时间内线程会不会完全停止的方法 等待的时间进程被打乱了 等待的时间到了]
threadpool/Shutdown.java
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500); //七秒钟之内是否完全运行完毕了
        executorService.shutdown();
        boolean b = executorService.awaitTermination(7L, TimeUnit.SECONDS);
        System.out.println(b);
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • shutdownNow [立刻关闭线程池]
///正在执行的和不要关闭 正在等待的内容直接返回 如何优雅编写?
package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 演示关闭线程池
 */
public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //任务往线程池中提交
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500); //七秒钟之内是否完全运行完毕了
        executorService.shutdownNow();//暴力关闭线程
        //这是正在队列中的数据 取了个List集合 都到runnableList里面了 要给它一个交代
        List<Runnable> runnableList = executorService.shutdownNow();
    }
}
class ShutDownTask implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断了");
        }
    }
}

=================================================================
pool-1-thread-6
pool-1-thread-4
pool-1-thread-10
pool-1-thread-8
pool-1-thread-3被中断了
pool-1-thread-1被中断了
pool-1-thread-8被中断了
pool-1-thread-2被中断了

暂停和恢复线程池

  • 拒绝时机
    • 当Executor关闭时,提交新任务被拒绝
    • 以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和
4种拒绝策略
  • AbortPolicy [直接抛出异常]
  • DiscardPolicy [默默的丢弃]
  • DiscardOldestPolicy [丢弃最老的]
  • CallerRunsPolicy [誰提交任务誰去跑(避免了业务损失 提交任务速度下降 给了线程池缓冲时间)]
钩子方法,给线程池加点料
  • 每个任务执行前后
  • 日志、统计
  • 代码演示
threadpool/PauseableThreadPool.java
package threadpool;

import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 演示每个任务执行前后放钩子函数
 */
public class PauseableThreadPool extends ThreadPoolExecutor {
    private final ReentrantLock lock = new ReentrantLock();
    private Condition unpaused = lock.newCondition();
    private boolean isPaused;
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override //在执行任务之前都会调用这个函数
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        lock.lock();
        //如果检测到就暂停休息
        try {
            while (isPaused) {
                unpaused.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void pause(){
        lock.lock();
        try{
            isPaused = true;
        }finally {
            lock.unlock();
        }
    }
    public void resume(){
        lock.lock();
        try{
            isPaused = false;
            unpaused.signalAll(); //唤醒全部
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10l,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("我被执行");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10000; i++) {
            pauseableThreadPool.execute(runnable);
        }
            Thread.sleep(1500);
            pauseableThreadPool.pause();
            System.out.println("线程池被暂停了");
            Thread.sleep(1500);
            pauseableThreadPool.resume();
            System.out.println("线程池被恢复了");
    }
}

线程池实现复用的原因

  • 线程池组成部分
    • 线程池管理器
    • 工作线程
    • 任务队列
    • 任务接口(Task)
Executor家族
  • 线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池相关的类,都是什么关系?

    Executor ← ExecutorService ← AbstractExecutorService ← ThreadPoolExecutor

线程池实现任务复用的原理
  • 相同线程执行不同任务

线程池状态和使用注意点

线程池状态
状态 含义
RUNNING 接收新任务并处理排队任务
SHUTDOWN 不接受新任务,但处理排队任务
STOP 不接受新任务,也不处理排队任务,并中断正在进行的任务
TIDYING 所有任务都已终止,workerCount为零时,线程会转换到TIDYING状态,并将运行terminate()钩子方法
TERMINATED terminate()运行完成
使用线程池的注意点
  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄露

ThreadLocal

ThreadLocal的两个作用
  • 让某个需要用到的对象在线程间隔离
  • 在任何方法中都可以轻松获取到该对象 .get

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问) 时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是ptivate static类型的,用于关联线程和线程上下文,它的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂性。

  • 定义:提供线程局部变量;一个线程局部变量在多个线程中,分别由独立的值(副本)
  • 特点:简单(开箱即用)、快速(无格外开销)、安全(线程安全)
  • 场景:多线程场景(资源持有、线程一致性、并发计算、线程安全等场景)
  • 实现原理:Java中使用哈希表实现
  • 应用范围:几乎所有提供多线程特征的语言
  • 设计者追求开箱即用的体验

ThreadLocal API

  • 构造函数 ThreadLocal()
  • 初始化 initialValue()
    • 该方法会返回当前线程对应的”初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
    • 当线程第一次使用get方法访问变量时,将调用此方法
    • 每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法
    • 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来**重写initialValue()**方法
  • 访问器 T get():得到这个线程对应的value,如果是首次调用get()则会调用initialize来得到这个值 // void set(T t):为这个线程设置一个新值
  • 回收 void remove(): 删除这个线程
根据共享对象的生成时机不同,选择initialValue或set来保存对象
场景一:initialValue
  • 在ThreadLocal第一次get的时候把对象給初始化出来,对象的初始化时机可以由我们控制
场景二:set
  • 如果需要保存到ThreadLocal里的对象的生成时机不由我们随机控制,例如拦截器生成的用户信息
  • 用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用
使用ThreadLocal带来的好处
  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效地利用内存、节省开销
  • 避免传参的繁琐
threadpool/ThreadLocalAPI.java
package threadpool;

public class ThreadLocalAPI {
    public static ThreadLocal<Long> x = new ThreadLocal(){
        @Override
        protected Long initialValue(){
            System.out.println("Inital Value run...");
            return Thread.currentThread().getId();
        }
    };

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                System.out.println(x.get());
            }
        }.start(); //调用一次initialValue 每个线程单独拥有一个
        x.set(101l);
        x.remove();//清空线程 结果为1 因为 Thread.currentThread().getId();
        System.out.println(x.get());//发现x.remove被移除了 结果去重新触发initialValue
        System.out.println(x.get());
    }
}
========================================================================
Inital Value run...
Inital Value run...
20
1
1

若注释掉x.remove则会出现结果:
Inital Value run...
101
101
20

ThreadLocal的4种核心场景

  • 持有资源——持有线程资源供线程的各个部分使用,全局获取,减少编程难度
  • 线程一致——帮助需要保持线程一致的资源(如数据库事务) 维护一致性,降低编程难度
  • 线程安全——帮助只考虑了单线程的程序库,无缝向多线程场景迁移

ThreadLocal并发场景分析01

例1 200QPS压测统计接口
  • 观察200QPS下Spring框架的执行情况
  • 目标:理解并发、竞争条件、临界区等概念
  • 代表场景:交易场景
并发、竞争条件和临界区

ThreadLocal并发场景

  • 并发:多个程序同时执行
  • 竞争条件:多个进程(线程)同时访问同一个内存资源,最终的执行结果依赖于多个进程执行时的精确时序
  • 临界区:访问共享内存的程序片段
StatController.java
package com.imooc.springbootlearn;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller //并发可能导致同时 数据错误 需要加★后面的代码
public class StatController {
    //★★ 避免用锁 导致当数据多的话线程缓慢
    static ThreadLocal<Integer> c = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    //★ 请求并发也要排队synchronized(但是不要轻易使用锁) 应该怎么办 见★★
    synchronized void _add() throws InterruptedException {
        Thread.sleep(100);
        c.set(c.get() + 1);
    }

    @RequestMapping("/stat")
    public Integer stat() {
        return c.get();
    }

    @RequestMapping("/add")
    public Integer add() throws InterruptedException {
//        Thread.sleep(100l);
//        c++;
        _add();
        return 1;
    }

}
SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages="com.imooc")
public class SpringBootlearnApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }
}
总结
  • 基于线程池模型synchronize(排队操作很危险)
  • 用ThreadLocal收集数据很快速且安全
  • 思考:如何在多个ThreadLocal中收集数据?

ThreadLocal场景分析——减少同步

ThreadLocal< T >同步
StatController.java    高效解决高并发
package com.imooc.springbootlearn;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.HashSet;

@Controller //并发可能导致同时 数据错误 需要加★后面的代码
public class StatController {

    static HashSet<Val<Integer>> set = new HashSet<>();
    synchronized static void addSet(Val<Integer> v){
        set.add(v);
    }

    //★★ 避免用锁 导致当数据多的话线程缓慢
    static ThreadLocal<Val<Integer>> c = new ThreadLocal<Val<Integer>>() {
        @Override
        protected Val<Integer> initialValue() {
            Val<Integer> v = new Val<>();
            v.set(0);
            //set.add(v);//容易产生同步问题 小锁一下
            return v;
        }
    };

    //★ 请求并发也要排队synchronized(但是不要轻易使用锁) 应该怎么办 见★★
/*synchronized*/void _add() throws InterruptedException {
        Thread.sleep(100); //高并发
        Val<Integer> v = c.get();
        v.set(v.get() + 1);
    }

    @RequestMapping("/stat")
    public Integer stat() {
        return set.stream().map(x -> x.get()).reduce((a, x) -> a + x).get();
    }

    @RequestMapping("/add")
    public Integer add() throws InterruptedException {
//        Thread.sleep(100l);
//        c++;
        _add();
        return 1;
    }
}
Val.java
package com.imooc.springbootlearn;

public class Val<T>{
    T v;
    public void set(T _v){
        v = _v;
    }
    public T get(){
        return v;
    }
}
总结
  • 完全避免同步()
  • 缩小同步范围(简单) + ThreadLocal解决问题
  • 思考:还可以用在哪些场景?
源码分析1-Quartz SimpleSemaphore

  • Quartz的SimpleSemaphore提供资源隔离(上锁)
  • SimpleSemaphore中的lockOwners(ThreadLocal)为重度锁操作前置过滤
  • 思考:学易,用难!
源码分析2-Mybatis框架保持连接池线程一致
什么是本地事务
  • A(Atomic)原子性,操作不可分割
  • C(Consistency)一致性,任何时刻数据都能保持一致
  • I(Isolation)隔离性,多事务并发执行的时许不影响结果
  • D(Durability)持久性,对数据结构的存储是永久的

Begin → [本地事务:(更新订单状态 →<订单状态更新成功>→发放资源) → 资源发放成功(提交) → Commit →<持久化>→DB。**若资源发放失败(回滚)**→Rollback]

源码分析03 Spring框架对分布式事务的支持

技术选型——实现自己的ThreadLocal

MyThreadLocal.java
package com.imooc.springbootlearn;

import java.util.HashMap;
import java.util.Objects;

class MyThreadLocal<T> {
    static HashMap<Thread, HashMap<MyThreadLocal<?>, Objects>> threadLocalMap = new HashMap<>();//这里会产生临界区

    synchronized static HashMap<MyThreadLocal<?>, Objects> getMap() { //这是锁临界区
        var thread = Thread.currentThread();
        if (!threadLocalMap.containsKey(thread)) {
            threadLocalMap.put(thread, new HashMap<MyThreadLocal<?>, Objects>());
        }
        return threadLocalMap.get(thread);
    }

    T value;

    protected T initialValue() {
        return null;
    }

    public T get() {
        var map = getMap();
        if (!map.containsKey(this)) {
            map.put(this, initialValue());
        }
        return (T) map.get(this);
    }

    public void set(T v){
        var map = getMap();
        map.put(this, v);
    }
}
Test.java
package com.imooc.springbootlearn;

public class Test {
    static MyThreadLocal<Long> v = new MyThreadLocal<Long>(){
        @Override
        protected Long initialValue() {
         return Thread.currentThread().getId();
        }
    };

    public static void main(String[] args) {
        for (Integer i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(v.get());
            }).start();
        }
    }
}
问题
  • HashMap中直接存储了MyThreadLocal的引用,导致内存无法回收
  • 思考:可以用整数ID替代对MyThreadLocal的引用
MyThreadLocal.java
package com.imooc.springbootlearn;

import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

class MyThreadLocal<T> {
    static AtomicInteger atomic = new AtomicInteger(); //保证数据唯一性
    Integer threadLocalHash = atomic.addAndGet(0x61c88647); //这个值保证hash的更平均性
    static HashMap<Thread, HashMap<Integer, Objects>> threadLocalMap = new HashMap<>();//这里会产生临界区

    synchronized static HashMap<Integer, Objects> getMap() { //这是锁临界区
        var thread = Thread.currentThread();
        if (!threadLocalMap.containsKey(thread)) {
            threadLocalMap.put(thread, new HashMap<Integer, Objects>());
        }
        return threadLocalMap.get(thread);
    }

    T value;

    protected T initialValue() {
        return null;
    }

    public T get() {
        var map = getMap();
        if (!map.containsKey(this.threadLocalHash)) {
            map.put(this.threadLocalHash, initialValue());
        }
        return (T) map.get(this.threadLocalHash);
    }

    public void set(T v){
        var map = getMap();
        map.put(this.threadLocalHash, v);
    }
}
  • HashMap无限增加?初始空间分配是否合理?

HashTable

源码解读——哈希表实现ThreadLocal
什么是哈希表?

哈希表(散列 HashTable) 根据键(Key) 访问/设置内存中存储的位置的值

总结

  • 架构是严密且精确的东西(切记夸夸其谈)
  • 并发是一个很危险的场景,提高能力才会获得安全感
  • 保持怀疑,持续学习
  • 会用 → 场景查找 → 轻量实现 → 源码对照 → 场景沉淀
  • 程序架构:低耦合(独立)、高内聚(组合做到开箱即用)
  • 无论达到什么高度,要永远认为自己是个菜鸡

分布式章节

  • 什么是分布式
  • 分布式的作用
  • 分布式和单体结构的对比
  • CAP定理

  • 集群、分布式、微服务的区别

什么是分布式

  • 利用物理架构形成多个自治的处理元素,不共享主内存,但是通过发送信息合作。

  • 饭店初始的例子
    • 一个厨师 [单例]
    • 多个厨师
    • 术业有专攻:配菜师、洗菜工
  • 实际项目的演进过程

    • 一个项目,大而全
    • 多台机器,部署相同的应用
    • 分布式:权限系统、员工系统、请假系统

分布式的作用

为什么需要分布式
  • 实际工作中的痛点
    • 工程臃肿 [相互耦合 相互冲突]
    • 测试、上线繁琐
    • 开发效率低
单体应用的问题
  • 应用代码耦合严重,功能扩展难
  • 新需求开发交互周期长,测试工作量大
  • 新加入的开发同事需要很长时间才能熟悉系统
  • 升级维护也很困难(改动任何一点地方都要升级整个系统)
  • 系统性能提升艰难,可用性低,不稳定
分布式的好处
  • 增大系统容量
  • 加强系统可用[某个模块出bug 但不影响其他]
  • 因为模块化,所以系统模块重用度更高
  • 因为软件服务模块被拆分,开发和发布速度可以并行而变得更快
  • 系统扩展性更高
  • 团队协作流程也会得到改善
  • 技术升级

单体和分布式的对比

分布式和单体结构的对比
传统单体构架 分布式构架
新人的学习成本 业务逻辑成本高 架构逻辑成本高
部署、运维 容易 发布频繁,顺序复杂、运维难
隔离性 一损俱损,殃及鱼池 故障影响范围小
架构设计 难度低 难度指数级上升
系统性能 响应快、吞吐量小 响应慢、吞吐量大
测试成本 很高
技术多样性 技术单一且封闭 技术多样且开放
系统扩展性 扩展性差 扩展性很好
系统管理成本 成本低 成本高

CAP定理

CAP的重要性

分布式不可能同时满足三个条件

CAP理论是什么?
  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果
  • A(Availability, 可用性):非故障节点应该在合理的时间内作出合理的响应(不是错误或超时的响应),但是可能不是最新的数据
  • P(Partition tolerance, 分区容错):当出现网络分区现象后,系统能够继续运行

CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的

集群、分布式、微服务的区别

集群和分布式的区别
  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上 [服务器之间要通信]
  • 集群:同一个业务,部署在多个服务器上 [五台机器可以不通信]
集群和微服务的区别
  • 集群:分散压力
  • 微服务:分散压力
微服务和分布式的区别
  • 微服务是架构设计方式 [逻辑架构]

  • 分布式是系统部署方式 [物理架构]

  • 微服务:是一种架构方式 [大的服务拆成小的服务 每个服务独立开发测试]

  • 分布式:主要强调部署的方式

Docker

  • 基本概念、用途、核心思想
  • Docker的组成、架构、重要概念
  • Docker的安装
  • 第一个Docker容器
  • 运用Nginx镜像,并访问到Docker容器内部 [从外部进行访问]
  • 制作自己的Docker容器,dockerfile实战

Docker的基本概念、用途、核心思想

  • Docker应用广泛
    京东618:15万个Docker实例,所有业务全部容器化
  • Docker是什么?
    [以前的图标是大鲸鱼拖着集装箱]
    • Docker是一个用来装程序及其环境的容器[类似于安装包.exe],属于Linux容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的Linux容器解决方案
    • 比喻:客车可以装人,衣柜可以放衣服 [Windows下写的小游戏也可以放在Docker中]

为什么需要Docker

  • 环境配置的难题
  • 虚拟机[资源占用很多 模拟一套完整系统但步骤多(有些步骤无法跳过) 启动慢]
  • Docker的基础——Linux容器 [体积小 速度快 轻量级虚拟机]

Docker的用途

  • 提供统一的环境
  • 提供快速拓展、弹性伸缩的云服务 [解决双十一淘宝+天猫 数据量剧增问题]
  • 防止其他用户的进程把服务器资源占用过多 [程序相互之间可以隔离]
  • 部署简单 运维简单 节省服务器资源

Docker的特点

标准化
  • 运输方式(把程序和环境从一个机器运送到另一个机器)

  • 存储方式(程序和环境的存储)

  • API接口(不需要Tomcat等应用的命令了,都标准化了)

  • 灵活:即使是最复杂的应用也可以集装箱化

  • 轻量级:容器利用并共享主机内核

  • 便携式:可以在本地构建,部署到云,并在任何地方运行

Docker带来的好处
  • 开发团队的好处 完全可以控制所有的环境[同一个镜像] 降低了风险

image镜像

  • 存储:联合文件系统,UnionFS

容器和仓库

  • 镜像类似于Java中的类,而容器就是实例化
  • 容器的这一层是可以修改的,而镜像是不可以修改的
  • 同一个镜像可以生成多个容器独立运行,而她们之间没有任何的干扰
仓库
client和deamon
  • client[客户端]:提供給用户一个终端,用户输入Docker提供的命令来管理本地或远程的服务器
  • deamon服务端守护进程,接收Client发送的命令并执行相应的操作

Docker的安装

更换系统镜像

云服务器管理控制台 (aliyun.com) → 实例 → 更多 → 更换操作系统 → CentOS 7.6 64位

C:\Users\Pluminary>ssh root@47.98.225.105
    
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:THXnakQ0Se5ee+d7oHO0NYKBTW7mhEKc426m9+rQgnk.
Please contact your system administrator.
Add correct host key in C:\\Users\\Pluminary/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in C:\\Users\\Pluminary/.ssh/known_hosts:7
Host key for 47.98.225.105 has changed and you have requested strict checking.
Host key verification failed.
    
//使用命令或手动删除 C:\\Users\\Pluminary/.ssh/known_hosts 防火墙拦截了
vim C:\\Users\\Pluminary/.ssh/known_hosts

C:\Users\Pluminary>ssh root@47.98.225.105
root@47.98.225.105's password:Panchunyao123!

[root@iZbp1dssknxftmjczbtpndZ ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
//yum地址换成国内yum源
   //wget-O 这是大写的字母O 含义是放到指定目录下 替换本地文件为国内文件
[root@iZbp1dssknxftmjczbtpndZ ~]# wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
[root@iZbp1dssknxftmjczbtpndZ ~]# yum clean all
[root@iZbp1dssknxftmjczbtpndZ ~]# yum makecache //读取新的源

//较旧的Docker版本称为docker或docker-engine。如果已安装这些程序,请卸载它们以及相关的依赖项。
[root@iZbp1dssknxftmjczbtpndZ ~]# yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
//如果yum报告未安装这些软件包,也没问题。
    //更新yum 先查看需要更新哪些
[root@iZbp1dssknxftmjczbtpndZ ~]# yum check-update
[root@iZbp1dssknxftmjczbtpndZ ~]# yum update
// 安装所需的软件包
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install -y yum-utils \
                  device-mapper-persistent-data \
                  lvm2
// 使用以下命令来设置稳定的存储库 sudo是用超级管理员
[root@iZbp1dssknxftmjczbtpndZ ~]# sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

// 查看docker版本
[root@iZbp1dssknxftmjczbtpndZ ~]# yum list docker-ce --showduplicates | sort -r
// 安装指定的版本
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install docker-ce-18.09.0 docker-ce-cli-18.09.0 containerd.io
// Docker 是服务器----客户端架构
// 命令行运行docker命令的时候,需要本机有 Docker 服务。用下面的命令启动
[root@iZbp1dssknxftmjczbtpndZ ~]# systemctl start docker
// 安装完成后,运行下面的命令,验证是否安装成功。
[root@iZbp1dssknxftmjczbtpndZ ~]# docker version 或者 docker info

第一个Docker容器

  • 下载镜像
  • docker pull [OPTIONS] NAME[:TAG]
  • docker images [OPTIONS] [REPOSITORY[:TAG]] 查看本机有没有任何镜像
[root@iZbp1dssknxftmjczbtpndZ ~]# docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete
Digest: sha256:53641cd209a4fecfc68e21a99871ce8c6920b2e7502df0a20671c6fccc73a7c6
Status: Downloaded newer image for hello-world:latest
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              d2c94e258dcb        11 months ago       13.3kB
  • 运行镜像
  • docker run [OPTIONS] IMAGE [COMMAND] [ARG…]
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal. 

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

后台运行容器

运行Nginx镜像,并访问到Docker容器内部
  • 前台、后台
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images //查看一下程序镜像是否还在
[root@iZbp1dssknxftmjczbtpndZ ~]# docker pull hub.c.163.com/library/nginx:1.13.0
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
hello-world                   latest              d2c94e258dcb        11 months ago       13.3kB
hub.c.163.com/library/nginx   1.13.0              46102226f2fd        6 years ago         109MB
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hub.c.163.com/library/nginx:1.13.0
//上面的没反应是正常情况,再开一个cmd窗口


//第二个窗口:另一个cmd窗口 
C:\Users\Pluminary>ssh root@47.98.225.105
root@47.98.225.105's password:
Last login: Sun Apr  7 16:30:40 2024 from 182.102.75.173

Welcome to Alibaba Cloud Elastic Compute Service !
//展示当前容器列表
[root@iZbp1dssknxftmjczbtpndZ ~]# docker ps
CONTAINER ID        IMAGE                                COMMAND                  CREATED              STATUS              PORTS               NAMES
298f7e19f6a8        hub.c.163.com/library/nginx:1.13.0   "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              kind_taussig
//这时回到第一个窗口 ctrl+c 关掉运行 这时去第二个窗口docker ps的时候已经没有了
// ↓↓↓↓↓↓ 要把Nginx在后台运行才可以解决上述问题 ↓↓↓↓↓↓
[root@iZbp1dssknxftmjczbtpndZ ~]# docekr run -d hub.c.163.com/library/nginx:1.13.0
f124fc9171824e508639768b800efba5c780385dc1d135ab0ff70d80d3d75510 //返回容器ID
//此时再去第二个窗口 docker ps就会看到 CONTAINER ID 有f124fc917182
//第一个窗口 查看容器内部的风景 -i让容器输入有效 t是給我们分配一个终端 bash是启动终端
[root@iZbp1dssknxftmjczbtpndZ ~]# docker exec -it f124 bash //让docker明白我们让哪个启动
root@f124fc917182:/#  //已经进入到容器内部
root@f124fc917182:/# pwd
/
root@f124fc917182:/# touch 1 //创建了一个1
root@f124fc917182:/# ls
1  bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@f124fc917182:/# which nginx
/usr/sbin/nginx

Docker三种网络模式 [网络会隔离 需要配置进行端口映射]

  • Bridge 网卡网络独立的端口
  • Host 同步宿主机的端口
  • None
  • 端口映射技术
实操
  • 访问Docker内的Nginx
cmd窗口2
[root@iZbp1dssknxftmjczbtpndZ ~]# docker stop f12
f12
[root@iZbp1dssknxftmjczbtpndZ ~]# docker ps //目前没有容器在运行了
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    //-p 本机的8080端口去映射内部的80端口 前面的是本机 后面的是容器内部的
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run -d -p 8080:80 hub.c.163.com/library/nginx:1.13.0
3906129ced72b668581a58dc36595b08734b82e247e2927eb2a8da6fbe7508ae
//拔出端口的信息
[root@iZbp1dssknxftmjczbtpndZ ~]# netstat -na|grep 8080
tcp6       0      0 :::8080                 :::*                    LISTEN

在网页上输入 http://47.98.225.105:8080/

Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.
Thank you for using nginx.

这个不是在宿主机启动的nginx 而是在容器中启动的【通过宿主机中转 并通过Bridge实现独立】
★★ 通过一台服务器上去布置多台Docker 而每台docker里面是独立的 并且可以通过一个端口 来实现和外界的映射关系 ★★
//这个与7行代码的区别 这里是大P 将这个容器的所有端口都进行映射
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run -d -P hub.c.163.com/library/nginx:1.13.0

制作自己的Docker容器,dockerfile实战

[把自己的软件程序打包好传給别人]

  • dockerfile作用
  • 使用dockerfile的好处[一目了然 哪个环境跑在什么环境下 非常方便知道如何配置]
  • 写一个自己的dockerfile

因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,也就是docker build命令,生成的可执行程序就是image,之后就可以运行这个image了,这就是docker run命令,image运行起来后就是docker container。

FROM alpine:latest #继承父类 alpine及其小的环境
MAINTAINER imooc   #描述这个镜像由誰维护的
CMD echo 'hello my dockerfile'  #进入任何想要的dockerfile命令
[root@iZbp1dssknxftmjczbtpndZ ~]# touch Dockerfile
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
Dockerfile
[root@iZbp1dssknxftmjczbtpndZ ~]# vim Dockerfile
//按小写i进入编辑模式  按ESC退出编辑模式 :wq会保存
//取名字 .dockerfile现在的路径
[root@iZbp1dssknxftmjczbtpndZ ~]# docker build -t hello_docker .
    //先发送到服务器
Sending build context to Docker daemon  285.7kB
    //从父类中拉取下来
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
4abcf2066143: Pull complete
Digest: sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Status: Downloaded newer image for alpine:latest
 ---> 05455a08881e
    //写定它的维护者
Step 2/3 : MAINTAINER imooc
 ---> Running in 38a73abca50e
Removing intermediate container 38a73abca50e
---> f44831f49afe
    //把这个语句写进去
Step 3/3 : CMD echo 'hello my dockerfile'
 ---> Running in 1ff431539208
Removing intermediate container 1ff431539208
 ---> f45b88d0cabc
Successfully built f45b88d0cabc
Successfully tagged hello_docker:latest

//运行自己的镜像
[root@iZbp1dssknxftmjczbtpndZ ~]# docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
hello_docker                  latest              f45b88d0cabc        3 minutes ago       7.38MB
alpine                        latest              05455a08881e        2 months ago        7.38MB
hello-world                   latest              d2c94e258dcb        11 months ago       13.3kB
hub.c.163.com/library/nginx   1.13.0              46102226f2fd        6 years ago         109MB
[root@iZbp1dssknxftmjczbtpndZ ~]# docker run hello_docker
hello my dockerfile

Nginx

  • Nginx介绍
  • Nginx的安装
  • 常用命令讲解和演示
  • 配置文件讲解
  • 场景实战:搭建一个静态文件的Nginx服务

Nginx介绍

  • Nginx是什么、适用场景
  • Nginx应用广泛
  • Nginx优点

Nginx是什么、适用场景

  • HTTP的反向代理服务器
  • 动态静态资源分离
正向代理

普通的请求转发,客户端把信息传递到代理服务器,代理服务器找到信息转发给我们
提供安全功能 代理服务器有防火墙,可以隐藏自身的信息

反向代理

提供安全和防火墙 在多个服务器后端提供负载均衡 为服务器提供缓存 把来自用户的压力平均分配[负载均衡]

动态静态资源分离 [加速访问 整体速度提高]
  • 不分离会变慢
  • 静态资源无需经过Tomcat,Tomcat只负责处理动态请求
  • 后缀为gif的时候,Nginx会直接获取到当前请求的文件并返回

Nginx应用广泛

Nginx的优点
  • 高并发、高性能
  • 可扩展性好
  • 高可靠性 [服务器运行可以达到数年之久]
  • 热部署
  • 开源、可商用

Nginx的安装

Nginx在CentOs安装
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install yum-utils
[root@iZbp1dssknxftmjczbtpndZ ~]# vim /etc/yum.repos.d/nginx.repo  //输入源信息 告诉它从哪下载
//输入:3-18行 配置源
[root@iZbp1dssknxftmjczbtpndZ ~]# [nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
    //然后查看源 列出可以使用的ngix地址
[root@iZbp1dssknxftmjczbtpndZ ~]# yum list | grep nginx
    //运行安装命令
[root@iZbp1dssknxftmjczbtpndZ ~]# yum install nginx 1:1.16.1-1.el7.ngx
    //查看版本,若出现版本号,则安装成功
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx -v
nginx version: nginx/1.24.0
用whereis nginx可以查看到目录:
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man8/nginx.8.gz

常用命令讲解和演示

[root@iZbp1dssknxftmjczbtpndZ ~]# /usr/sbin/ngix 启动
[root@iZbp1dssknxftmjczbtpndZ ~]# systemctl stop docker //把之前的docker关停
[root@iZbp1dssknxftmjczbtpndZ ~]# ps -aux | grep nginx //提取相关进程 80是默认端口
[root@iZbp1dssknxftmjczbtpndZ ~]# nginx -h //提取帮助
    
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /etc/nginx
[root@iZbp1dssknxftmjczbtpndZ nginx]# ls //里面有nginx.conf配置文件 conf.d中也有一个defalut.conf
conf.d  fastcgi_params  mime.types  modules  nginx.conf  scgi_params  uwsgi_params
[root@iZbp1dssknxftmjczbtpndZ nginx]# cd conf.d/
    
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c   //读取指定配置文件
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -s stop //停止nginx
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c /etc/nginx/nginx.conf //以这个文件启动nginx

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -t //发布前的测试
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -s stop
[root@iZbp1dssknxftmjczbtpndZ conf.d]# /usr/sbin/nginx //开启nginx
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -t //查看使用的哪个配置文件
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -v //打印版本信息
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -V //打印详细参数
    
[root@iZbp1dssknxftmjczbtpndZ conf.d]# whereis uginx//找nginx的路径
nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man8/nginx.8.gz
[root@iZbp1dssknxftmjczbtpndZ conf.d]# ps aux | grep nginx //打印进程信息

    //-s信号 1.stop立即停止 2.quit优雅停止 3.reload重启[优雅停止quit] 4.reopen更换日志文件

配置文件讲解

  • 语法
    • “; “ 结尾
    • “{}” 组织多条指令
    • “include” 引入
    • ‘’#’’ 注释
  • 实操演示
  • 默认配置文件分析
    • nginx.conf
    • default.conf
[root@iZbp1dssknxftmjczbtpndZ conf.d]# cat /etc/nginx/nginx.conf //打开文件配置
nginx.conf配置文件讲解
首先我们进入到cd etc/nginx.然后通过ls查看nginx目录的相关内容。在nginx目录下,我们需要关注nginx.conf文件,这个文件是我们的主配置文件,cat打开:
cat nginx.conf 【效果显示在下方分割线后】

[root@iZbp1dssknxftmjczbtpndZ conf.d]# cd /usr/share/nginx/html
[root@iZbp1dssknxftmjczbtpndZ html]# vim stst.html //新建文件
[root@iZbp1dssknxftmjczbtpndZ html]# ls
50x.html     index.html
[root@iZbp1dssknxftmjczbtpndZ html# vim test.html //新建一个html文件保存下来
//i编辑       hello nginx           esc + :wq
[root@iZbp1dssknxftmjczbtpndZ conf.d]# nginx -c /etc/nginx/nginx.conf //启动!
//如果不显示一定是服务器规则组的问题  自定义 TCP    目的:1/65535   源:0.0.0.0/0
网址输入:http://47.98.225.105/test.html 
============================================================================
# 运行用户,默认是nginx
user  nginx;
# nginx进程数,一般设置为和cpu核数一样
worker_processes  1;

# 全局错误日志路径
error_log  /var/log/nginx/error.log warn;
# 进程pid路径
pid        /var/run/nginx.pid;
 
events {
# 最大连接数
    worker_connections  1024;
}

# 设置http服务器
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
# 设置日志的格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志的路径
    access_log  /var/log/nginx/access.log  main;

# 开启高效传输模式
    sendfile        on;
    #tcp_nopush     on;
# 长连接超时时间,单位是秒
    keepalive_timeout  65;
#传输时是否压缩,压缩的话需要解压,但是传的大小就小了
    #gzip  on;
#加载其他的配置文件,一带多
    include /etc/nginx/conf.d/*.conf;
}
============================================================================

搭建静态资源服务器

搭建一个静态文件的Nginx服务
  • 实操演示
  • 配置和网页文件作为教辅提供 [静态网页]
//scp传输文件 -r每一个文件夹都传递归 /usr/share/nginx/web/ 上传到这个文件夹
cmd本机框
C:\Users\Pluminary> scp -r /Users/Pluminary/Desktop/静态资源/静态网页/ root@47.98.225.105:/usr/share/nginx/web/

cmd服务器框
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/share/nginx/web/
[root@iZbp1dssknxftmjczbtpndZ web]# pwd //查看现在位置
/usr/share/nginx/web
[root@iZbp1dssknxftmjczbtpndZ web]# ls
css  fonts  images  index.html  js
//修改文件 可以让nginx访问到文件 访问web下面的内容
[root@iZbp1dssknxftmjczbtpndZ web]# vim /etc/nginx/conf.d/default.conf  
//修改location:.../web
===============================================================
location / {
        root   /usr/share/nginx/web;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/web;
    }
===============================================================
[root@iZbp1dssknxftmjczbtpndZ web]# nginx -t //测试一下是否有问题
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
[root@iZbp1dssknxftmjczbtpndZ web]# nginx -s reload //重启服务
//浏览器访问:http://47.98.225.105/index.html
[root@iZbp1dssknxftmjczbtpndZ web]# cat /etc/nginx/nginx.conf
//进去看到 access_log  /var/log/nginx/access.log  main; 打开它
[root@iZbp1dssknxftmjczbtpndZ web]# cat /var/log/nginx/access.log //打开日志文件

RabbitMQ

  • 初识RabbitMQ
  • RabbitMQ的安装和启动 [Erlang语言]
  • RabbitMQ管理后台
  • 实战案例演示
  • 交换机工作模式 [fanout、direct、topic、headers]
  • SpringBoot整合RabbitMQ

初识RabbitMQ

  • 核心思想:接收并转发消息。类似于邮局

  • producer信息生产者

  • queue队列

  • consumer会从queue中获取消息

消息队列

  • 什么是消息队列
  • MQ(Message Queue)
消息队列的性质
  • 业务无关 [不需要考虑上层业务模型]
  • FIFO [先进先出]
  • 容灾 [对于消息队列可以持久化 断电后也可以保存数据后重新发送]
  • 性能
为什么要用消息队列
  • 系统解耦
  • 异步调用 [我告诉你要做 我可以不等你做完 我再返回 相互不影响]
    [点外卖 用户发送外卖请求給中间件,随后MQ帮助分发后续流程(扣钱、召唤骑手 计算天气、时间等)]
  • 流量削峰 [MQ把请求先存在队列中 以合适的速度发送 化解压力]

RabbitMQ的特点和核心概念

  • 开源、跨语言
  • Erlang语言编写 [交换机、通信方面] [数据复制与转发性能好]
  • 应用广泛
  • 社区活跃、API丰富
AMQP协议
  • Advanced Message Queuing Protocol 高级消息队列协议
RabbitMQ核心概念
  • Server:服务
  • connection:与Server建立连接
  • channel:信道,几乎所有的操作都在信道上进行,客户端可以建立多个信道
  • message:消息,由properties和body组成
  • virtual host:虚拟主机,顶层隔离。同一个虚拟主机下,不能由重复的交换机和queue
  • Exchange:交换机,接收生产者的信息的,然后根据指定的路由器把消息转发到所绑定的队列上
  • binding:绑定交换机和队列
  • routing key:路由键,路由规则,虚拟机可以用它来确定这个消息如何进行一个路由
  • queue:队列,消费者只需要监听队列来消费消息,不需要关注消息来自于哪个Exchange
  • Exchange和Message Queue存在着绑定的关系,一个Exchange可以绑定多个消息队列

RabbitMQ的安装和启动

  • 安装Erlang
  • 安装RabbitMQ
  • 启动RabbitMQ
安装Erlang
  • 安装erlang-rpm包,该包经过RabbitMQ官方处理
  • 使用Erlang Solutions源进行安装
  • 使用EPEL(“Extra Packages for Enterprise Linux”)进行安装
我们先准备两个安装包://★★★★别看下面胡咧咧 直接教辅中有erlang和rabbitmq与教材相同★★★★
RabbitMQ: https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.13
Erlang:https://packages.erlang-solutions.com/erlang-solutions-2.0-1.noarch.rpm

注意: 如果说你选的是最新的版本那么你就要选取对应支持的 erlang 的版本才行,具体看下面的连接
https://www.rabbitmq.com/which-erlang.html

这些都准备好之后我们使用 Xptf 软件把这个安装包传输到 linux 端对应文件下(自己建一个文件夹)即可。

安装
第一步:
我们第一步先要对 linux 端进行一个 erlang 的环境配置,所以我们要先解压 erlang 包。
    # rpm -Uvh erlang-solutions-2.0-1.noarch.rpm
    # yum install -y erlang
安装完成后,查看一下版本号,能查到说明就是安装好了
    # erl -v

第二步:
在安装一下
    # yum install -y socat

第三步:
接下来我们就需要进行 RabbitMQ 的安装了

    # rpm -Uvh rabbitmq-server-3.8.33-1.el8.noarch.rpm 
    # yum install rabbitmq-server -y

安装完之后我们来启动测试一下:

    # systemctl start rabbitmq-server
    # systemctl status rebbitmq-server

最后一步:
安装跑起来之后我们设置一下开机自启动

    # systemctl enable rabbitmq-server
    # systemctl stop rabbitmq-server   # 关闭开机自起
=========================================================
可以在这些目录中查找RabbitMQ的配置文件、启动脚本等。另外,RabbitMQ的启动脚本通常会被安装到/usr/sbin或/usr/bin目录下。你可以尝试使用以下命令来查找RabbitMQ的启动脚本位置:

find /usr/sbin /usr/bin -name 'rabbitmq*'
=========================================================
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/sbin
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl start rabbitmq-server
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl status rabbitmq-server
● rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2024-04-08 23:24:39 CST; 38min ago
 Main PID: 3570 (beam.smp)
===================================================================================
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl enable rabbitmq-server
[root@iZbp1dssknxftmjczbtpndZ sbin]# systemctl stop rabbitmq-server   # 关闭开机自起

Linux环境如何彻底卸载感干净RabbitMQ_linux卸载rabbitmq-CSDN博客

停止RabbitMQ
$rabbitmqctl stop

设置开机启动
$ systemctl enable rabbitmq-server 

启动RabbitMQ
$ systemctl start rabbitmq-server

看看端口有没有起来,查看状态

$ rabbitmqctl status 

要检查RabbitMQ服务器的状态,请运行:

systemctl status rabbitmq-server

开启web管理界面
rabbitmq-plugins enable rabbitmq_management

RabbitMQ管理后台

启动RabbitMQ
  • 启动RabbitMQ:systemctl start rabbitmq-server
  • 查看状态:rabbitmqctl status
  • 启动管理台
  • 配置admin用户
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmq-plugins enable rabbitmq_management
Enabling plugins on node rabbit@iZbp1dssknxftmjczbtpndZ:
rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@iZbp1dssknxftmjczbtpndZ...
Plugin configuration unchanged. ##添加账户
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmqctl add_user admin password
Adding user "admin" ...            ##设置管理员
[root@iZbp1dssknxftmjczbtpndZ ~]# rabbitmqctl set_user_tags admin administrator
Setting tags for user "admin" to [administrator] ...
浏览器进入RabbitMQ管理后台 输入 http://47.98.225.105:15672/
RabbitMQ管理后台
  • 浏览页面
  • 添加用户
  • 创建虚拟主机(Virtual Hosts) => 都有各自的队列、交换机

第一个生产者

实战案例演示
  • 新建项目
  • Hello World
    P(生产者)→hello→C(消费者)
  • 创建一个rabbitmq(Maven)新项目
  • rabbitmq支持多语言
登录上RabbitMQ后台 上面Admin 右面Virtual Hosts 
点表里的 /  下面增加一个admin

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>20</maven.compiler.source>
        <maven.compiler.target>20</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.29</version>
        </dependency>
    </dependencies>

</project>
helloworld/send.java
package helloworld;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * Hello World的发送类,连接到RabbitMQ服务端,然后发送一条消息后退出
 */
public class send {
    //队列名字
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //发布消息
        String message = "Hello World!";
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + message);
        //关闭连接
        channel.close();
        connection.close();
    }
}
helloworld/Recv.java
package helloworld;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;

/**
 * 接收消息,并打印,持续运行
 */
public class Recv {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("开始接收消息");
        //接收消息并消费 2:是否确认收到(快递签收 自动消息确认) 3:消息收到后进行处理
        channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
            //收到信息后会执行的函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:" + message);
            }
        });
    }
}
=======================================================
//由于是持续接收消息的 若将send.java中改为Hello World2 将会持续打印到控制台
开始接收消息
收到消息:Hello World!
收到消息:Hello World2!

根据消息内容做处理

多个消费者分担压力

RabbitMQ后台http://47.98.225.105:15672/

循环调度
workqueues/NewTask.java
package workqueues;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 生产者:生产批量消息
 */
public class NewTask {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME,true,false,false,null);
        for (int i = 0; i < 10; i++) {
            String message = i + "...";
            channel.basicPublish("",TAKS_QUEUE_NAME,null,message.getBytes("UTF-8"));
            System.out.println("发送了消息:" + message);
        }
        //关闭连接
        channel.close();
        connection.close();
    }
}
=============================================================================================
发送了消息:0...
发送了消息:1...
发送了消息:2...
发送了消息:3...
发送了消息:4...
发送了消息:5...
发送了消息:6...
发送了消息:7...
发送了消息:8...
发送了消息:9...
workqueues/Worker.java
package workqueues;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者,接收前面的批量消息
 */
public class Worker {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME, true, false, false, null);
        System.out.println("开始接收消息");
        channel.basicConsume(TAKS_QUEUE_NAME, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到了消息:" + message);
                try{
                    doWork(message);
                }finally {
                    System.out.println("完成消息处理");
                }
            }
        });
    }

    private static void doWork(String task) {
        //有点延迟1秒
        char[] chars = task.toCharArray();
        for (char ch : chars) {
            if (ch == '.') {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
==========================================================================================
收到了消息:0...
完成消息处理
收到了消息:1...
完成消息处理
收到了消息:2...
完成消息处理
收到了消息:3...
完成消息处理
收到了消息:4...
完成消息处理
收到了消息:5...
完成消息处理
收到了消息:6...
完成消息处理
收到了消息:7...
完成消息处理
收到了消息:8...
完成消息处理
收到了消息:9...
完成消息处理
并行 多个消费者平均压力!!!
公平派遣[因为奇数偶数压力不同] + 消息确认 【以工作量的程度去分任务
//在Worker消费方打开并行消费 Run/Debug Configurations → Modify options蓝字 → Allow multiple instances             此时两个woker开始配合平均压力 一起工作
workqueues/Worker.java
package workqueues;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者,接收前面的批量消息
 */
public class Worker {
    private final static String TAKS_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ地址
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        //建立连接
        Connection connection = factory.newConnection();
        //获得信道
        Channel channel = connection.createChannel();
        //声明队列 参数:第二个会不会随着重启消失 第三个队列是否仅能給连接使用 第四个未使用自动删除? 第五个参数
        channel.queueDeclare(TAKS_QUEUE_NAME, true, false, false, null);
        System.out.println("开始接收消息");
        channel.basicQos(1); //最希望处理的数量 处理完之前不会接收下一个任务
        channel.basicConsume(TAKS_QUEUE_NAME, false, new DefaultConsumer(channel) { //关掉自动接收 要手动确认false
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到了消息:" + message);
                try{
                    doWork(message);
                }finally {
                    System.out.println("完成消息处理");
                    channel.basicAck(envelope.getDeliveryTag(), false); //false 不同时一起确认 手动确认消息
                }
            }
        });
    }

    private static void doWork(String task) {
        //有点延迟1秒
        char[] chars = task.toCharArray();
        for (char ch : chars) {
            if (ch == '.') {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
=======================================================================================
开始接收消息
收到了消息:1
消息处理完成
收到了消息:2...
消息处理完成
......

交换机工作模式

  • fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的
  • direct:根据RoutingKey匹配消息路由到指定队列
  • topic:生产者指定RoutingKey消息根据消费端指定的队列通过模糊匹配的方式进行相应转发
  • headers:根据发送消息内容中的headers属性来匹配
fanout模式
广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的 [所有消息无差别发送]

fanout/EmitLog.java【生产者】
package fanout;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 发送日志信息
 */
public class EmitLog {
    //创建交换机
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        String message = "info: Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + message);
        channel.close();
        connection.close();
    }
}
fanout/ReceiveLogs.java【消费者】
package fanout;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 描述:接收日志消息 有多个接收同样的消息
 */
public class ReceiveLogs {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        //非持久会自动删除的队列 在同一个类中多次启动 每一次队列名字都不一样
        String queueName = channel.queueDeclare().getQueue();
        //交换机和队列的绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("开始接收消息");
        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
direct模式
根据RoutingKey匹配消息路由到指定队列 [消费者接收消息不一致]

direct/ReceiveLogsDirect1.java
package direct;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * 接收3个等级的日志
 */
public class ReceiveLogsDirect1 {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //生成一个随机的临时的queue
        String queueName = channel.queueDeclare().getQueue();

        //一个交换机同时绑定三个queue
        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");
        channel.queueBind(queueName, EXCHANGE_NAME, "error");

        System.out.println(" 开始接收消息");

        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
=============================================================================
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
direct/ReceiveLogsDirect2.java
package direct;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;

/**
 * 接收1个等级的日志
 */
public class ReceiveLogsDirect2 {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //生成一个随机的临时的queue
        String queueName = channel.queueDeclare().getQueue();

        //一个交换机同时绑定两个queue,比刚第一个Receiver少接收了一个error等级
        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");

        System.out.println("开始接收消息");

        Consumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("收到消息:"+message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
=============================================================================
收到消息:Hello World!
收到消息:Hello World!
收到消息:Hello World!
direct/EmitLogDirect.java
package direct;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class EmitLogDirect {

    private static final String EXCHANGE_NAME = "direct_log";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机 DIRECT
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String message = "Hello World!";
        //发送第一个等级消息
        channel.basicPublish(EXCHANGE_NAME, "info",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为info,消息内容:" + message);

        //发送第二个等级消息
        channel.basicPublish(EXCHANGE_NAME, "warning",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为warning,消息内容:" + message);

        //发送第三个等级消息
        channel.basicPublish(EXCHANGE_NAME, "error",null, message.getBytes("UTF-8"));
        System.out.println("发送了消息:" + "等级为error,消息内容:" + message);
        channel.close();
        connection.close();
    }
}
=============================================================================
发送了消息:等级为info,消息内容:Hello World!
发送了消息:等级为warning,消息内容:Hello World!
发送了消息:等级为error,消息内容:Hello World!
topic模式
比如消息严重性怎么样、只想记录error模块的用户信息
  • ***** 可以替代一个单词
  • # 可以替代零个或多个单词

【发送者/生产者】
topic/EmitLogTopic.java
package topic;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

/**
 * topic模式交换机,发送消息
 */
public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) {
        Connection connection = null;
        Channel channel = null;
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("47.98.225.105");
            factory.setUsername("admin");
            factory.setPassword("password");

            connection = factory.newConnection();
            channel = connection.createChannel();

            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            String message = "Animal WorldroutingKey";

            String[] routingKeys = new String[9];
            routingKeys[0] = "quick.orange.rabbit";
            routingKeys[1] = "lazy.orange.elephant";
            routingKeys[2] = "quick.orange.fox";
            routingKeys[3] = "lazy.brown.fox";
            routingKeys[4] = "lazy.pink.rabbit";
            routingKeys[5] = "quick.brown.fox";
            routingKeys[6] = "orange";
            routingKeys[7] = "quick.orange.male.rabbit";
            routingKeys[8] = "lazy.orange.male.rabbit";
            for (int i = 0; i < routingKeys.length; i++) {
                channel.basicPublish(EXCHANGE_NAME, routingKeys[i], null,
                        message.getBytes("UTF-8"));
                System.out.println("发送了:" + message + ":" + routingKeys[i]);
            }


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ignore) {
                }
            }
        }
    }
}
==================================================================================
发送了:Animal WorldroutingKey:quick.orange.rabbit
发送了:Animal WorldroutingKey:lazy.orange.elephant
发送了:Animal WorldroutingKey:quick.orange.fox
发送了:Animal WorldroutingKey:lazy.brown.fox
发送了:Animal WorldroutingKey:lazy.pink.rabbit
发送了:Animal WorldroutingKey:quick.brown.fox
发送了:Animal WorldroutingKey:orange
发送了:Animal WorldroutingKey:quick.orange.male.rabbit
发送了:Animal WorldroutingKey:lazy.orange.male.rabbit
【接收者/消费者Ⅰ(对"*.orange.*" 数据感兴趣)】
package topic;

import com.rabbitmq.client.*;

import java.io.IOException;
/**
 * 特定路由键
 */
public class ReceiveLogsTopic1 {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        //指定bindingKey 最重要的一步!
        String bindingKey = "*.orange.*";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);

        System.out.println("开始接收消息");

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                    AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(
                        " 接收消息: " + message + ":" + envelope.getRoutingKey());
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
==================================================================================
开始接收消息
 接收消息: Animal WorldroutingKey:quick.orange.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.elephant
 接收消息: Animal WorldroutingKey:quick.orange.fox
【接收者/消费者Ⅱ(对"*.*.rabbit" 和 "lazy.#" 数据感兴趣)】
package topic;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
/**
 * 特定路由键
 */
public class ReceiveLogsTopic2 {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("47.98.225.105");
        factory.setUsername("admin");
        factory.setPassword("password");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        //指定bindingKey
        String bindingKey1 = "*.*.rabbit";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey1);
        String bindingKey2 = "lazy.#";
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey2);

        System.out.println("开始接收消息");

        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                    AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(
                        " 接收消息: " + message + ":" + envelope.getRoutingKey());
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}
==================================================================================
开始接收消息
 接收消息: Animal WorldroutingKey:quick.orange.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.elephant
 接收消息: Animal WorldroutingKey:lazy.brown.fox
 接收消息: Animal WorldroutingKey:lazy.pink.rabbit
 接收消息: Animal WorldroutingKey:lazy.orange.male.rabbit
  • headers:根据发送消息内容中的headers属性来匹配

Spring Boot整合RabbitMQ

  • 实操代码演示

创建两个Springboot项目
spring-boot-rabbitmq-consumer 和 spring-boot-rabbitmq-consumer
在application.properties中设置 如果是本机用户名和密码都是guest
spring.rabbitmq.username=guest

项目:spring-boot-rabbitmq-producer [消息发送者/生产者]
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>spring-boot-rabbitmq-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-rabbitmq-consumer</name>
    <description>spring-boot-rabbitmq-consumer</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties
spring.application.name=spring-boot-rabbitmq-producer
server.port=8080
spring.rabbitmq.addresses=127.0.0.1:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
com/imooc/springbootrabbitmqproducer/MsgSender.java
package com.imooc.springbootrabbitmqproducer;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 发送消息
 */
//@Component让springboot发现
@Component
public class MsgSender {
    @Autowired
    private AmqpTemplate rabbitmqTemplate;
    public void send1(){
        String message = "This is message 1, routing key is dog.red";
        System.out.println("发送了:" + message);
        //指定交换机的名字  routing key is dog.red  消息内容
        this.rabbitmqTemplate.convertAndSend("bootExchange","dog.red",message);
    }
    public void send2(){
        String message = "This is message 2, routing key is dog.black";
        System.out.println("发送了:" + message);
        //指定交换机的名字  routing key is dog.red  消息内容
        this.rabbitmqTemplate.convertAndSend("bootExchange","dog.black",message);
    }
}
com/imooc/springbootrabbitmqproducer/TopicRabbitConfig.java
package com.imooc.springbootrabbitmqproducer;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * rabbitmq配置类
 */
@Configuration
public class TopicRabbitConfig {
    @Bean
    public Queue queue1(){
        return new Queue("queue1");
    }
    @Bean
    public Queue queue2(){
        return new Queue("queue2");
    }
    //用一个Bean定义交换机
    @Bean
    TopicExchange exchange(){
        return new TopicExchange("bootExchange");
    }

    //有了Q1 Q2后去指定一个topic交换机
    //绑定到交换机上
    @Bean
    Binding bingdingExchangeMessage1(Queue queue1, TopicExchange exchange){
        //to绑定到哪个交换机 with指定routingKey
        return BindingBuilder.bind(queue1()).to(exchange).with("dog.red");
    }
    @Bean
    Binding bingdingExchangeMessage2(Queue queue2, TopicExchange exchange){
        //to绑定到哪个交换机 with指定routingKey
        return BindingBuilder.bind(queue2()).to(exchange).with("dog.#");
    }
}
用test的测试类
com/imooc/springbootrabbitmqproducer/SpringBootRabbitmqProducerApplicationTests.java
package com.imooc.springbootrabbitmqproducer;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootRabbitmqProducerApplicationTests {

    @Autowired
    MsgSender msgSender;

    @Test
    public void send1(){
        msgSender.send1();
    }
    @Test
    public void send2(){
        msgSender.send2();
    }
}
项目:spring-boot-rabbitmq-consumer [消息接收者/消费者]
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>spring-boot-rabbitmq-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-rabbitmq-consumer</name>
    <description>spring-boot-rabbitmq-consumer</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties
spring.application.name=spring-boot-rabbitmq-consumer
server.port=8081
spring.rabbitmq.addresses=127.0.0.1:15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
Receiver1.java
package com.imooc.springbootrabbitmqconsumer;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者1
 */
@Component
@RabbitListener(queues = "queue1")
public class Receiver1 {
    @RabbitHandler //收到这个消息后怎么处理
    public void process(String message){
        System.out.println("Receiver1:" + message);
    }
}
Receiver2.java
package com.imooc.springbootrabbitmqconsumer;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者2
 */
@Component
@RabbitListener(queues = "queue2")
public class Receiver2 {
    @RabbitHandler //收到这个消息后怎么处理
    public void process(String message){
        System.out.println("Receiver2:" + message);
    }
}

先启动消费者 然后再启动生产者test中的测试
就可以发现
send1() → 发送了:This is message 1, routing key is dog.red
send2() → 发送了:This is message 2, routing key is dog.black

阅读全文

Java面试

2024/3/28

Java面试 —— 准备

給职场新人的建议

  • 不要有为老板工作的心态
  • 永远不要敷衍工作
  • 事情做完马上汇报
  • 管理好自己的情绪,做一个可爱的人
  • 接受任务有确认机制
  • 注意工作汇报方式
  • 注重细节
  • 提建议要有数据支撑
  • 給主管选择题 而不是判断题
  • 要提升自己的价值 [人脉网]
  • 认真对待工作汇报3

介绍面试环节中常见问题

自我介绍
  • 我是誰
  • 我做过什么
  • 我的优势在哪里

张总你好,我叫潘春尧,今年23岁,来自河北唐山人,来应聘贵公司的Java软件工程师职位,26年毕业于南昌工学院软件工程专业,从事Java开发有2年的工作时间了,我对编程有着浓厚的兴趣,擅长基于HTML5的互联网应用开发,擅长使用Mysql数据库对大数据量方面的调优有经验,同时具备完整的项目开发能力,具备一定的分布式架构经验,之前我做过两个项目,都是独立完成的,第一个是XX 在2024年开发,这是一个电商网站,已经运用了一年的时间,我负责底层的架构搭建与前后端开发,技术方面采用了XXXX。架构方面底层使用了XXX,我负责的模块设计XXX,这些都是由我完整开发的,而第二个项目是由我开发的xxx,这是一个xxx平台,基于阿里云搭建,技术方面与上一个相似,工作原因产品需要增加XXX,我负责的网站模块主要负责xxx,这也是由我负责开发的,以上是我的个人介绍,有不足的请您多保函。

现场面对面 [要面试官知道我有闪光的价值]
  • 容貌要求 [注意形象(刮胡子,短发,指甲)]
  • 着装要求 [品牌+面料+皮鞋(商务休闲)]
  • 面试礼仪 [提前15分钟到达面试场地填写表格、敬语(您好,感谢,再见)、善意的微笑] [和前台了解面试官的称呼和职位 主动握手 张总… ] [跟着面试官进去后 注意关门 进去前注意敲门] [沟通时眼神不要游离] [提交简历调转180°双手呈递] [不要抖腿翘腿] [手放腿上 十指交叉放桌子上 不许靠着椅子靠背]
  • 一对多面试 [一男一女 男: 技术主管[考察技术能力] 女: HR[根据动作分析本人能力]] [面试紧张的时候要降低语速]
面试必知必会
  • 谈谈你的职业规划 [对未来职业有清晰的规划]

我从小对软件技术有着很浓厚的兴趣,在未来我五年内我希望从事于与技术相关的岗位,我给自己定位了一个小目标,在五年的时间内我希望拿下系统架构师的认证,同时在分布式、微服务的领域有自己独道的见识。希望在未来的一段时间里公司能给我提供成长的机会。

  • 与同时遇到正面冲突,你会怎么办

[不诋毁任何人 不传达任何负面情绪 以大局为重]
我很少与别人产生正面的冲突,面对别人对我个人的职责批评,我通常先会反思我是不是真的做错了,如果是我会虚心接受,尽快改正。如果是别人对我无端的诽谤,在公共场合也会保持沉默,我会在私下跟他心平气和的沟通看看之间是不是有什么误会。如果是在工作中就某些问题产生了分歧。在会议时直接打开天窗说亮话,阐述自己的观点,这种是典型的对事不对人,这也是点到为止,如果双方意见僵持不下,这可能需要项目经理在中间做出定夺,他看到的问题可能比我们的更加全面而且更有建设性,总之我处事的原则是对事不对人,做事必须认真,做人还是要谦虚一些

  • 你生活中最重要的是什么

[考察你对待工作 生活 家庭是什么态度] [强调以事业为重]
我一个人来到XX打拼,目前是单身,对于我现在而言,事业就是我的全部,我会珍惜每一天,让自己尽快的成长起来

  • 这份工作你能遇见哪些问题

[不能太过实在 避重就轻 强调自己的特性]
我对咱们公司的业务不太了解,经验很少,进入公司以后我会听取领导的指示和要求,尽快的熟悉公司的业务和技能,并且制定一份近期的工作计划并报告給领导批准,最后按照计划展开工作,其实针对这个问题,明眼人都看得出来,都说了一堆套话,废话,但是对于面试官来说,你正面的回答了这些问题。而这些问题肯定是现实中存在的,这样既无伤大雅又巧妙的表达了自己对这份工作的期望

  • 你希望与什么样的同事或上级公事

[考察情商]
作为刚步入社会的新人,我应该要求自己多熟悉工作环境适应环境,而不是对环境提出要求,对于我的工作只要能发挥我的专长就可以了,如果工作中有做的不对的地方还请领导多批评指正

  • 你将如何开展工作

[面试通过几率已经很大了]
每天我会提高工作效率,完成公司交付的工作任务,任务未完成绝不下班,根据领导的指示和工作安排制定好工作计划,提前预备并按照计划完成,多请示领导及时汇报,如果有遇到不明白的地方要向前辈们请教,抓住缝隙时间多学习多提升,尽快的提高自己的专业能力与业务水平

  • 你有想过创业吗

[面试官喜欢有冲劲的小伙子 给出明确的时间规划 不要说我是来公司学习的]
我当然有想过创业,但我觉得不是现在,我计划在30岁前后选择创业,软件行业需要大量的技术积累才能有竞争力,同时呢创业对我来说也需要很多的资金,我是一名刚走出校园的新人,在这两方面都不成熟,所以在未来的5年时间内,我会努力的工作,在经验和资金方面做好准备,同时之所以来贵公司,是因为贵公司在XXX行业是一个知名的企业,我想在为公司创造价值的同时,也能进一步的掌握咱们行业最新的动向,希望您能给我一次证明自己的机会,我肯定不会让公司失望的。

  • 你还应聘了哪些公司

[千万不能说海投 也不能说hr打了电话才来 最好不要跨行业]
做一些准备了

  • 为什么你的学历是专科

[很难回答 送命题 学历非常重要的 话术把劣势转换优势]
实话实说上高中的时候我并不是一个好学生,在那时候爱玩电脑,确实也耽误了不少学业。但我认为学历的高低并不代表能力的好坏,在我上了专科以后我更加意识到因为学历比别人落后,自己就更需要努力了,从小对编程感兴趣,上了大学后就更有时间朝着自己喜欢的领域努力。在大学几年的时间里面我没有醉生梦死,自己呢也连续学习了XXX技术内容,我知道有朝一日,这些知识一定能给我带来用处,我一定能成为所向往的软件工程师,我能用实际行动来证明专科不比本科差

  • 谈谈你对加班的看法

[测试你是否能为公司奉献 要强调上班提高效率 强调加班并不是我的工作效率低下]
上班时间我会提高效率,做到当日事当日毕,但是如果当天的事没有按时完成,我会义不容辞的加班,现在我是单身,没有家庭的负担,可以全身心的工作

  • 你的薪资的要求

[提前有明确主张 自己能赚多少钱 给出hr谈薪资的余地期待8k 可以报价8.5k 千万不要说区间6-8k]
我期望的薪资是8.5k,每一个公司都有自己的薪资结构,咱们公司是xxx行业中是非常有实力的企业,相对于薪资来说我更看重技术和行业本身,如果您觉得薪资水平超出了公司的预期的话,这个咱们可以谈

  • 谈谈你对跳槽的看法

[千万不要说负面话语 也不能说工资太低、福利太差、没有晋升空间]
我这个人稳定性很好,本身反对频繁跳槽,但是有一种情况我会选择跳槽,就是公司的发展对于我的事业规划不符,我之前在进入xxx公司的时候,公司主要从事金融行业的项目开发,后来该行业金融不理想改作了电信行业了,当我的长期规划是想成为长期的金融软件工程师与架构师,如果遇到这种情况我会好不容易的选择离开。

Java面试——Java基础面试技巧

JDK、JRE、JVM有什么区别

  • JDK:Java Development Kit 针对Java程序员的产品
  • JRE:Java Runtime Environment是运行Java的环境集合
  • JVM:Java虚拟机用于运行Java字节码文件[class],跨平台的核心。通过JVM隐藏了操作系统最底层的API,对于我们java工程师只需要面对jvm层面进行开发,再转移到其他就可以了
为什么可以跨平台?

因为JVM可以跨平台,JVM只认识字节码,能够解释到系统的API调用,对于不同的系统有不同的JVM实现。Windows和Linux有不同的JVM实现,但是编成字节码后都是一样的。

常用数字类型的区别

名次 取值范围 存储空间
字节(byte) -$2^7$ ~ $2^7$-1
-128 ~ 127
1个字节
短整数(short) -$2^{15}$ ~ $2^{15}$-1
-32768 ~ 32767
2个字节
整数(int) -$2^{31}$ ~ $2^{31}$-1
-2147483648 ~ 2147483647
4个字节
长整数(long) -$2^{63}$ ~ $2^{63}$-1 8个字节
单精度(float) $2^{-149}$ ~ $2^{128}$-1 4个字节
双精度(double) $2^{-1074}$ ~ $2^{1024}$-1 8个字节

Float在JVM的表达方式及使用陷阱

float d1 = 423432423f;
float d2 = d1 + 1;
if(d1 == d2){
    System.out.println("d1==d2");
}else{
    System.out.println("d1!=d2");
}

答案:d1 == d2
对于单精度浮点型float d1在内存中是采用科学计数法表示
表达为:4.2343242E7 保留小数点后七位 [如果更高精度的用double]
d1是八位数
无论是d1还是d2不考虑最后一位
d2用科学计数法表示同样为4.2343242E7
因此 d1 == d2

银行业务数值精确计算要用BigDecimal类进行加减乘除的计算

随机数的使用

编程题:随机生成30~100之间的整数
==========================================================
package com.example.demo12;

import java.util.Random;

public class RandomSample {
    public Integer randomInt1(){
        int min = 30;
        int max = 100;
        //生成正整数范围0-70的随机数 => 30~100
        int result = new Random().nextInt(max - min) + min;
        return  result;
    }
    public Integer randomInt2(){
        int min = 30;
        int max = 100;
        int result = (int)(Math.random() * (max-min)) + min;
        return result;
    }

    public static void main(String[] args) {
        System.out.println(new RandomSample().randomInt1());
        System.out.println(new RandomSample().randomInt2());
    }
}
编程题:列出1-1000的质数[大于1的情况下 只能被1和自身整除的数]
==========================================================
package com.example.demo12;

public class PrimeNumber {
    public static void main(String[] args) {
        for (int i = 2; i <= 1000; i++){
            boolean flag = true;
            for (int j = 2; j < i; j++){
                if(i % j == 0){
                    flag = false;
                    break;
                }
            }
            if (flag){
                System.out.println(i);
            }
        }
    }
}

面向对象的三大特征是什么

  • 封装
    对同一类事务的特征和功能包装到一起,只对外暴露需要调用接口而已
    [我想让你看到的你能看到 不想让你看到的不能看到]。作用:对接口进行实现的过程中每一个接口的实现类对接口进行了实现,但是在调用的时候通常是面对接口的,对于使用者来说只需要知道接口对应的哪些方法做什么用的就可以了,对内部的什么是不需要理解的。接口是体现封装的常见方法
    封装的好处

    1. 实现专业的分工
    2. 减少代码耦合
    3. 可以自由修改类的内部结构
  • 继承
    是Java中面向对象最显著的特征,继承是从已有的类中派生出新的类,新的类能够吸收原有属性的属性和行为并扩展新的类,Java中类是不支持多继承的,接口可以,一个接口可以继承多个其他接口 父类是子类的抽象化,子类是父类的具体化。
    接口与抽象类有那些异同?

    相同点 不同点
    都是上层的抽象 抽象类可包含方法的实现
    接口则只能包含方法的声明
    不能被实例化 [不可new] 继承类只能继承一个抽象类
    实现类可以实现多个接口
    都可以包含抽象发发 抽象级别:接口>抽象类>实现类

    作用不同:接口用于约束程序行为。继承则用于代码复用
    注意:JDK8以上版本,接口可以有default方法,包含方法实现

  • 多态
    最重要的特性 前面是为多态服务的
    多态是三大特性中最重要的操作
    多态是同一个行为具有多个不同表现形式或形态的能力
    多态是同一个接口,使用不同的实例而执行不同操作

静态和实例变量(方法)的区别

  • 语法区别:静态变量前要加static关键字,实例则不用
  • 隶属区别:实例变量属于某个对象的属性,而静态属于类
  • 运行区别静态变量在JVM加载类字节码创建,实例变量在实例化对象时创建
静态变量

在类中定义,使用static关键字修饰
静态方法:可以通过类名直接访问,也可以通过对象访问
调用上下文:不依赖于类的实例,不能访问实例变量和实例方法
生命周期:从类加载到类卸载,生命周期与类相同

实例变量

在类中定义,不适用static关键字修饰
实例方法:只能通过对象访问
调用上下文:依赖于类的实例,可以访问实例变量和实例方法
生命周期:从对象创建到对象被垃圾回收,生命周期与对象相同

类的执行顺序

package com.example.text;
//请写出程序输出结果
//1. 静态优先
//2. 父类优先
//3. 非静态块优先于构造函数
public class ExecutionSequence {
    public static void main(String[] args) {
        new GeneralClass();
    }
}
class ParentClass{
    static {
        System.out.println("①我是父类静态块");
    }

    {
        System.out.println("②我是父类非静态块");
    }
    public ParentClass(){
        System.out.println("③我是父类构造函数");
    }
}

class GeneralClass extends ParentClass{
    static{
        System.out.println("④我是子类静态块");
    }

    {
        System.out.println("⑤我是子类非静态块");
    }

    public GeneralClass(){
        System.out.println("⑥我是子类构造函数");
    }
}
/*
①我是父类静态块
④我是子类静态块
②我是父类非静态块
③我是父类构造函数
⑤我是子类非静态块
⑥我是子类构造函数
*/

总结
静态块:静态块在类加载时执行,且父类静态块先于子类静态块执行。
非静态块和构造函数:在对象创建时,先执行父类的非静态块和构造函数,再执行子类的非静态块和构造函数。

请说明java的异常体系

Error → Throwable
RuntimeException → Exception → Throwable
运行时产生的异常不要求对其强制try catch…

Error和Exception的区别和联系
Exception Error
可以是可被控制或不可控制的 总是不可控制的
表示一个由程序员导致的错误 经常用来用于表示系统错误或低层资源的错误
表示在应用程序级被处理 如果可能的话,应该在系统级被捕捉

String与字符串常量池 [比较内存地址]

String s1 = "abc";
String s2 = "abc";
String s3 = "abc"+"def";
String s4 = "abcdef";
String s5 = s2 + "def";
String s6 = new String("abc");
System.ouut.println(s1==s2); //true
System.ouut.println(s3==s4); //true 相等的都保存到常量池
System.ouut.println(s4==s5); //false 
//s5中的s2属于引用类型 在java编译期间无法确定其类型 只有在运行时才能确定具体的值 会产生一个新的内存地址给s5 所以不相等
System.ouut.println(s4.equals(s5));//true equals比较的是内容
System.ouut.println(s1==s6); //false
//s1是在常量池的常量 new String("abc");创建的东西不在常量池 存储的地方是完全不同的

String、StringBuilder、StringBuffer的区别

String StringBuffer StringBuilder
执行速度 最差 其次 最高
线程安全 线程安全 线程安全 线程不安全
使用场景 少量字符串操作 多线程环境下的大量操作 单线程环境下的大量操作

优先使用哪个?
单线程环境:优先使用 StringBuilder。因为 StringBuilder 没有线程同步开销,性能更高。
多线程环境:优先使用 StringBuffer。因为 StringBuffer 提供了线程安全的保证,避免了多线程环境下的并发问题

List与Set的区别

List Set
允许重复
是否允许null
是否有序
常用类 ArrayList
LinkedList
HashSet
LinkedHashSet
TreeSet

ArrayList与LinkedList的区别

ArrayList LinkedList
存储结构 基于动态数组 基于链表
遍历方式 连续读取 基于指针
使用场景 大数据量读取 频繁新增、插入[写入类型]

HashSet和TreeSet的区别

HashSet TreeSet
排序方式 不能保证顺序 按预置规则顺序
底层存储 基于HashMap 基于TreeMap
底层实现 基于Hash表实现 基于二叉树实现

List排序的编码实现

编程题:List排序
Employee.java
package com.example.text;

public class Employee {
    private String ename;
    private Integer age;
    private Float salary;

    @Override
    public String toString() {
        return "Employee{" +
                "ename='" + ename + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}';
    }

    public Employee() {}
    public Employee(String ename, Integer age, Float salary) {
        this.ename = ename;
        this.age = age;
        this.salary = salary;
    }
    public String getEname() { return ename;}
    public void setEname(String ename) {this.ename = ename;}
    public Integer getAge() { return age;}
    public void setAge(Integer age) {this.age = age;}
    public Float getSalary() {return salary;}
    public void setSalary(Float salary) {this.salary = salary;}
}
ListSorter.java
package com.example.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class ListSorter {
    public static void main(String[] args) {
        List<Employee> emps = new ArrayList<>();
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
        Collections.sort(emps, new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                //以薪资排序 如果是以年龄的话o1.getAge() - o2.getAge();
                return (int) (o1.getSalary() - o2.getSalary());
                //返回正数 第一个大于第二个
            }
        });
        System.out.println(emps);
    }
}

TreeSet排序的编码实现

编程题:TreeSet排序
Employee.java 同上
[自定义排序]
SetSorter.java 
package com.example.text.TreeSet;

import java.util.*;

//自定义排序
public class SetSorter {
    public static void main(String[] args) {
        TreeSet<Employee> emps = new TreeSet<Employee>(new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                return (int)(o2.getSalary() - o1.getSalary());
            }
        });
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
    }
}
[自然排序]
package com.example.text;

public class Employee implements Comparable<Employee>{
    private String ename;
    private Integer age;
    private Float salary;

    @Override
    public String toString() {
        return "Employee{" +
                "ename='" + ename + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}';
    }
    @Override
    public int compareTo(Employee o){
 //如果返回-1 Employee对象返回到红黑树的左边 采用降序排列
 //如果返回1
        return this.getAge().compareTo(o.getAge());
//      return o.getAge().compareTo(this.getAge()); 输出降序
//如果前面比后面大 返回1 反之返回-1 如果两者相等返回0
    }
    public Employee() {}
    public Employee(String ename, Integer age, Float salary) {
        this.ename = ename;
        this.age = age;
        this.salary = salary;
    }
    public String getEname() { return ename;}
    public void setEname(String ename) {this.ename = ename;}
    public Integer getAge() { return age;}
    public void setAge(Integer age) {this.age = age;}
    public Float getSalary() {return salary;}
    public void setSalary(Float salary) {this.salary = salary;}
}
=============================================================
SetSorter.java 
package com.example.text.TreeSet;

import java.util.*;

//自定义排序
public class SetSorter {
    public static void main(String[] args) {
        TreeSet<Employee> emps = new TreeSet<Employee>
        emps.add(new Employee("张三", 33, 1800f));
        emps.add(new Employee("李四", 55, 3800f));
        emps.add(new Employee("王五", 40, 2300f));
        System.out.println(emps);
    }
}

hashCode与equals的联系与区别

Object类hashCode()和equals()的区别
  • equals()方法用来判断两个对象是否”相同”

  • hashCode()返回一个int,代表”将该对象的内部地址” [如果不相等肯定不是同一个对象] [小概率情况下不同的对象也产生相同的hashCode(), 之后再用equals()去比较]

    序号 描述
    1 两个对象如果equals()成立, hashcode()一定成立
    2 如果equals()不成立, hashcode()可能成立
    3 如果hashcode()成立, equals()不一定成立
    4 hashcode()不相等, equals()一定不成立

hashCode 方法用于快速定位哈希表中的桶(bucket),而 equals 方法用于确认桶中的具体对象。通过 hashCode 快速过滤掉大部分不相等的对象,可以显著提高查找效率。
通常是先调用 hashCode 方法来定位桶,然后再调用 equals 方法来确认具体对象。equals 方法不会直接调用 hashCode 方法,而是依赖于 hashCode 方法的一致性。

Java IO中有集中类型的流

输入流 输出流
字节流 InputStream[父类]
FileInputStream
BufferedInputStream[缓存]
OutputStream[父类]
FileOutputStream
BufferedOutputStream[缓存]
字符流 Reader
FileReader
InputStreamReader
BufferedReader
Writer
FileWeiter
OutputStreamWriter
BufferedWeiter

★ 利用IO实现文件复制 ★ [笔试]

编程题:复制文件到指定文件夹
package com.example.text.FileCopy;

import java.io.*;

public class FileCopy {
    public static void main(String[] args) {
        //1. 利用JavaIo
        File source = new File("D:/application.rar");
        File target = new File("D:/target/application.rar");
        InputStream input = null;
        OutputStream output = null;
        try {
            input = new FileInputStream(source);
            output = new FileOutputStream(target);
            byte[] buf = new byte[1024];
            int byteRead;
            while((byteRead = input.read(buf)) != -1){
                output.write(buf,0,byteRead);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                input.close();
                output.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        //2. 利用FileChannel实现文件复制
        //3. Commons Io组件实现文件复制
        //FileUtils.copyFile(Source,Target);
    }
}

请介绍JVM的内存组成

[共享区:供所有对象共享 所有线程都访问到的地方]
:垃圾回收销毁处理区
方法区:虚拟机加载的类的信息、常量、静态变量等信息

[私有区:所有对象都是私有的]
程序计数器PC: 当前线程所执行的字节码指令的行号指示器[分值、跳转、循环、异常处理、线程恢复] 每个线程都有单独的程序计数器 [私有的]
虚拟机栈VM Stack:为Java的方法服务 当线程创建的时候虚拟机栈会为线程分配一个虚拟机栈,在线程执行的时候创建的每个方法都会创建一个栈针,用于存放局部变量、操作方法等 [入栈+出栈]
本地方法栈:类似于虚拟机栈,本地方法栈是执行本地方法使用

请简述Java的垃圾回收(GC)

  • GC(Garbage Collection)用于回收不再使用的内存
  • GC负责3项任务:分配内存、确保引用、回收内存
  • GC回收的依据某对象没有任何引用、则可以被回收
垃圾回收(GC)算法
  • 引用计数算法 [计数器 引用到这个对象的时候+1 被释放或断开计数器-1 当计数器变成0就可以被垃圾回收]
    若两个对象相互引用就会产生循环计数器 这个对于引用计数算法是无效的
  • 跟踪回收算法 [利用JVM维护的对象引用图 每个对象彼此依赖和引用 从根节点标记对象引用图和可以被标记的对象 遍历结束以后未被标记的对象是不能够使用的对象可以被回收]
  • 压缩回收算法 [将JVM堆中活动的对象放到一个集中的区域中 另外一大块留出一个活动的区域 对堆中的碎片进行集中的处理]
  • 复制回收算法 [把堆分成两个相同大小的区域 只有其中的一个区域被使用 直到这个区域被消耗完 垃圾回收器就会中断这个程序的运行 以遍历的方式把活动的对象移动到另一个区域 移动的过程中对象是紧密挨在一起的 可以消除其中的碎片 【在复制的同时也对对象进行重新复制和安排 一次性解决了内存碎片的问题】【需要两倍大小的内存空间 降低了效率】]
  • 按代回收算法 [把堆分成多个子堆,每个堆命名成一代,算法优先收集年幼年轻的对象,在这其中对象仍然存活的就把它移动到高级堆中 【减少扫描次数和范围 提高回收效益】]

Java的内存泄露的场景

请列举Java中内存泄漏的场景

一个不需要使用的对象还在内存中占用并使用其内存空间,忘记释放就会存在内存泄露

  • 静态集合类 static描述对象 存储在方法区 垃圾回收器不会对其扫描回收
  • 各种连接 JVM一直可达不会被回收
  • 监听器 全局存在的
  • 不合理的作用域 能用在方法中public 不要用在方法外private
  • 没有把对象及时设置为null【极少发生】

请实现对象的浅复制与深复制

  • 浅复制:只对对象及变量值进行复制,引用对象地址不变
  • 深复制[序列化方式]:创建一个新的对象,并且复制所有引用类型字段的对象。修改新对象的引用类型字段不会影响原对象。[彼此互不干扰]
com/example/text/clone/Dancer.java
package com.example.text.clone;

import java.io.*;

public class Dancer implements Cloneable,Serializable{
    private String name;
    private Dancer partner;

    public Dancer() {
    }

    public Dancer(String name, Dancer partner) {
        this.name = name;
        this.partner = partner;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dancer getPartner() {
        return partner;
    }

    public void setPartner(Dancer partner) {
        this.partner = partner;
    }
    public Dancer deepClone() throws IOException, ClassNotFoundException {
        //序列化, 将内存中的对象序列化为字节数组
        //序列化要注意加Serializable接口
        ByteArrayOutputStream bos = new ByteArrayOutputStream();//字节数组
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);

        //反序列化, 将字节数组转回到对象, 同时完成深复制的任务
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis); //字节数组还原成对象
        return (Dancer) ois.readObject();
    }
    //初始化一下
    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Dancer d1 = new Dancer();
        d1.setName("小明");
        Dancer d2 = new Dancer();
        d2.setName("小红");
        d1.setPartner(d2);
        //hashCode()是内存地址转化后的一个整数
        System.out.println("Partner:" + d2.hashCode());
        //浅复制
        Dancer shallow =  (Dancer)d1.clone();
        System.out.println("浅复制:" + shallow.getPartner().hashCode());
        //深复制
        Dancer deep = (Dancer) d1.deepClone();
        System.out.println("深复制:" + deep.getPartner().hashCode());
    }
}
简述一下equalsIgnoreCase和equals的区别
equalsIgnoreCase:比较两个字符串的内容是否相等, 忽略大小写
equals:比较两个字符的内容是否完全相等, 包括大小写
equalsIgnoreCase 和 equals 都是 String 类的方法,用于比较两个字符串是否相等(返回值都是Boolean)。它们的主要区别在于对大小写的处理方式。

Java面试——Web基础与数据处理

请说明Servlet执行流程

问:Servlet什么时候被实例化的呢?

Servlet默认情况下是第一次访问的时候实例化
也可以通过web.xml配置load-on-startup,使其在服务启动的时候实例化

问:Servlet在并发条件下如何处理的?

基于单例多线程技术提供web服务
使用servlet时不允许使用存在状态改变的变量

补充:

单例多线程技术
单例模式:Servlet 实例在 Web 容器中通常是单例的,即一个 Servlet 类只有一个实例。这意味着所有的请求都会共享同一个 Servlet 实例。
优点:减少了对象的创建和销毁开销,提高了性能。
缺点:需要特别注意线程安全问题,因为多个请求可能会同时访问同一个 Servlet 实例。

多线程:Web 容器为每个请求分配一个独立的线程来处理。当多个请求同时到达时,这些请求会被分配到不同的线程中,但这些线程都共享同一个 Servlet 实例。
线程安全:由于多个线程共享同一个 Servlet 实例,因此必须确保 Servlet 实例中的代码是线程安全的。

状态改变的变量
状态改变的变量:指的是在 Servlet 实例中定义的成员变量(实例变量),这些变量在不同请求之间可能会被修改。

线程安全问题如果多个线程同时访问和修改同一个成员变量,可能会导致数据不一致、竞态条件等问题。
竞态条件(Race Condition)是指在多线程或多进程环境中,由于多个线程或进程对共享资源的访问顺序不确定,导致程序的行为依赖于这些线程或进程的执行顺序,从而产生不可预测的结果。简单来说,竞态条件发生在多个线程或进程竞争同一资源时,如果这些线程或进程的执行顺序不同,会导致不同的结果。

Servlet生命周期

  • 启动时装载 - web.xml
  • 创建 - 构造函数
  • 初始化 - init() [专门的初始化资源]
  • 提供服务 - service() [对于发来的请求无论是get post 一律使用service进行处理] [细化分出doget() dopost()]
  • 停止时销毁 - destory() [销毁资源]
com/example/text/servlet/SampleServlet.java
package com.example.text.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SampleServlet extends HttpServlet {
    public SampleServlet(){
        System.out.println("正在创建Servlet");
    }

    @Override
    public void init() throws ServletException {
        System.out.println("正在初始化Servlet");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println(this.hashCode() + ": Service()正在提供服务");
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().print(this.hashCode() + ": Service()正在提供服务");
    }

    @Override
    public void destroy(){
        System.out.println("正在销毁Servlet");
    }
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>SampleServlet</servlet-name>
        <servlet-class>com.example.text.servlet.SampleServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>SampleServlet</servlet-name>
        <url-pattern>/sample</url-pattern>
    </servlet-mapping>
</web-app>

=================================================
http://localhost:8080/sample

正在创建Servlet 【启动时】
正在初始化Servlet
1025518765: Service()正在提供服务
正在销毁Servlet 【停止时】

请求与响应的结构

  • HTTP请求包含三部分:请求行、请求头、请求体

HTTP请求三部分

  • HTTP响应包含三部分:响应行、响应头、响应体

HTTP响应三部分

请求转发与响应重定向的区别

请求转发 [一次请求]
  • 请求转发是服务器跳转,只会产生一次请求
  • 请求转发语句是:request.getRequestDispatcher().forward()

请求转发

网页请求Tomcat的Servlet1,Servlet1请求Servlet2,Servlet2响应给网页,网页收到响应

响应重定向 [两次请求]
  • 重定向则是浏览器端跳转,会产生两次请求
  • 响应重定向语句是:response.sendRedirect()

响应重定向

网页$请求_1$Tomcat的Servlet1,Servlet1直接$响应_1$给网页,网页收到$响应_1$[让网页创建一个新的$请求_2$]
网页新创了一个$请求_2$Tomcat的Servlet2,Servlet2只对请求进行处理返回给$响应_2$网页

请阐述Session的原理

简述Session的原理

浏览器第一次向Tomcat发送请求,Tomcat收到请求自动在tomcat运行内存中生成一个存储空间并赋予一个SessionId:7U809J唯一编号,若使用了session.setAttribute(name,小潘),会在那个运行内存中存放一个小潘的内存属性[name:小潘] 当程序处理完后返回给浏览器其中包含了一个特殊信息SessionId:7U809J,浏览器收到响应后把SessionId:7U809J保存到浏览器的Cookie中SessionId:7U809J[只要当前浏览器没有被关闭的话 Cookie会一直存在 会在发送请求的时候一并发送给服务器],当第二次发送请求的时候浏览器会把SessionId:7U809J第二次发送给Tomcat,若使用了session.getAttribute(name) = 小潘,得到的结果就出现了小潘 返回浏览器响应

JSP九大内置对象是什么

内置对象 描述
request 请求对象 - HttpServletRequest
response 响应对象 - HttpServletResponse
session 用户会话 - HttpSession
application [全局实例] 应用全局对象 - ServletContext
out 输出对象 - PrintWriter
page 当前页面对象 - this [Object实例]
pageContext 页面上下文对象 - PageContext
config [全局参数] 应用配置对象 - ServletConfig
exception 应用异常对象 - Throwable

Statement 和 PreparedStatement的区别

  • PreparedStatement是预编译的SQL语句,效率高于Statement
  • PreparedStatement支持 ‘?’ 操作符,相对于Statement更加灵活
  • PreparedStatement可以防止SQL注入,安全性高于Statement
package com.example.text.jdbc;

import java.sql.*;

public class StatementSample {
    //使用Statement sql语句解释N多次
    //SQL注入攻击:  ' or 1=1 or '
    public void findByEname1(String ename) {
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";
        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(URL, username, password);
            Statement ps = conn.createStatement();
            //使用Statement
            String sql = "SELECT * FROM emp where ename = '" + ename + "' ";
            System.out.println(sql);
            ResultSet rs = ps.executeQuery(sql);

            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package com.example.text.jdbc;

import java.sql.*;

public class StatementSample {

    //使用预编译PreparedStatement sql语句解释一次
    public void findByEname2(String ename) {
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";

        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(URL, username, password);

            //使用预编译PreparedStatement
            String sql = "SELECT * FROM emp where ename = ?";
            System.out.println(sql);
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, ename);
            ResultSet rs = ps.executeQuery();

            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        StatementSample sample = new StatementSample();
        sample.findByEname1("SMITH' or 1=1 or '"); //SQL注入攻击
        sample.findByEname2("SMITH");
    }
}

请说明JDBC的使用步骤

  • 加载JDBC驱动
  • 创建数据库连接(Connection)
  • 创建命令(Statement)
  • 处理结果(ResultSet)
  • 关闭连接
package com.example.text.jdbc;

import java.sql.*;

public class MysqlJDBC {
    public static void main(String[] args) {
        //连接需要提前下载好包   mysql-connector-java-5.1.47.jar
        String driverName = "com.mysql.jdbc.Driver";
        String URL = "jdbc:mysql://127.0.0.1:3306/scott";
        String sql = "SELECT * FROM emp";
        String username = "root";
        String password = "root";
        Connection conn = null;
        try {
            //1.加载JDBC驱动
            Class.forName(driverName);
            //2.建立连接
            conn = DriverManager.getConnection(URL, username, password);
            //3.创建命令(Statement)
            Statement ps = conn.createStatement();
            //4.处理结果
            ResultSet rs = ps.executeQuery(sql);
            while (rs.next()) {
                System.out.println(rs.getString("ename") + "," +
                        rs.getString("job") + "," +
                        rs.getFloat("sal"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //5.关闭连接
        finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

SQL编程训练

scott.sql文件已准备好

★按部门编号升序、工资倒序的排列员工信息
SELECT * FROM emp ORDER BY deptno ASC, sal DESC

★列出deptno=30的部门名称及员工[关联查询]
SELECT dept.dname, emp.* FROM emp,dept
WHERE emp.deptno = dept.deptno
AND dept.deptno = 30

★列出每个部门最高、最低、平均工资 #若再列出人数只需要再SELECT后面加 COUNT(*)
SELECT deptno, max(sal), min(sal), avg(sal)
FROM emp 
GROUP BY deptno  

★列出市场部(SALES)及研发部(RESEARCH)的员工
SELECT * FROM dept d, emp e
WHERE d.deptno = e.deptno 
AND (d.dname = 'SALES' or d.dname = 'RESEARCH')

★列出人数超过3人的部门 #按照部门分组 
# WHERE对原始数据进行筛选[在GROUP BY之前进行的] 
# HAVING对分组后的进行二次筛选[在GROUP BY之后进行的]
SELECT d.dname, count(*)
FROM dept d, emp e
WHERE d.deptno = e.deptno 
GROUP BY d.dname 
HAVING count(*)>3

★计算MILLER年薪比SMITH高多少
SELECT a.a_sal, b.b_sal, a.a_sal - b.b_sal FROM 
(SELECT sal * 12 a_sal FROM emp WHERE ename = 'MILLER') a,
(SELECT sal * 12 b_sal FROM emp WHERE ename = 'SMITH') b

★★列出直接向King汇报的员工
SELECT * FROM emp
WHERE mgr = (SELECT empno FROM emp WHERE ename = 'KING')
#或
SELECT e.* FROM emp e, (SELECT empno FROM emp WHERE ename = 'KING') k
WHERE e.mgr = k.empno

★★列出公司所有员工的工龄,并倒序排列 
# NOW获取当前时间 
# SELECT DATE_FORMAT(date,format)   DATE_FORMAT只对日期形式的数据生效
SELECT * FROM(
    SELECT emp.*, DATE_FORMAT(NOW(),"%Y") - DATE_FORMAT(hiredate,"%Y") yg 
    FROM emp
)d 
ORDER BY d.yg DESC

★★计算管理者与基层员工平均薪资差额
SELECT a_avg - b_avg 
FROM
 (SELECT avg(sal) a_avg 
 FROM emp
 WHERE job = 'MANAGER' OR job = 'PRESIDENT') a,
  (SELECT avg(sal) b_avg
  FROM emp
  WHERE job IN ('CLERK','SALESMAN','ANALYST'))b

Java面试 —— 主流框架

谈谈你对IOC和DI的理解

  • IOC控制反转,设计理念,由第三方来管理与控制对象
  • DI依赖注入,具体实现,由对象容器在运行时动态注入对象

[DI是一种具体的技术实现,是对宏观IOC里面的一种技术上的形式,在Spring中使用了反射+工厂模式来实现DI]

IDAO dao = new UserDao(); //原始版本 原本的容器控制器

运行时Spring动态进行创建 对程序进行有效的解耦 //将控制权交给第三方控制容器  IOC容器
// applicationContext最底层是类工厂模式
IDAO dao = (IDAO)applicationContext.getBean("userDAO"); //ICO容器 动态实例化DAO
 <bean id = "userDAO" class = "com.imooc.dao.UserDAO"/>
 <bean id = "userDAO" class = "com.imooc.dao.UserExtDAO"/> //方便随时更改

Spring中Bean实例化有几种方式

  • 使用类构造器实例化
  • 使用静态工厂方法实例化
  • 使用实例工厂方法实例化
com/example/text/spring/Person.java
package com.example.text.spring;

public class Person {

    private String name;
    private Integer age;


    public Person() {
        System.out.println("Person默认构造函数");
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;

        System.out.println("Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}');
    }
}
com/example/text/spring/PersonStaticFactory.java
package com.example.text.spring;

/**
 * Person静态工厂
 */
public class PersonStaticFactory {
    public static Person createPerson(String name,int age){
        return new Person(name,age);
    }
}
com/example/text/spring/PersonFactory.java
package com.example.text.spring;

/**
 * Person实例工厂 实例化以后才可以调用
 */
public class PersonFactory {
    public Person createPerson(String name,int age){
        return new Person(name,age);
    }
}
C:\Users\Pluminary\Desktop\text\src\main\resources\applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--构造函数实例化-->
    <!-- 1.调用无参构造函数 -->
    <bean id="person1" class="com.imooc.spring.instance.Person"></bean>

    <!-- 2.调用有参构造函数 -->
    <bean id="person2" class="com.imooc.spring.instance.Person">
        <constructor-arg name="name" value="枫叶(构造函数)"/>
        <constructor-arg name="age" value="23"/>
    </bean>


    <!-- 通过静态工厂创建对象-->
    <bean id="person3" class="com.imooc.spring.instance.PersonStaticFactory" factory-method="createPerson">
        <constructor-arg name="name" value="蓝天(静态工厂)"/>
        <constructor-arg name="age" value="27"/>
    </bean>

    <!-- 通过实例工厂创建对象-->
<!-- 实例化PersonFactory-->
    <bean id="instanceFactory" class="com.imooc.spring.instance.PersonFactory"></bean>

    <bean id="person4" factory-bean="instanceFactory" factory-method="createPerson">
        <constructor-arg name="name" value="绿地(实例工厂)"/>
        <constructor-arg name="age" value="29"/>
    </bean>

</beans>
com/example/text/Application.java
package com.example.text;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
    }
}
===================================================================
Person默认构造函数
Person{name='枫叶(构造函数)',age=23}
Person{name='蓝天(静态工厂)',age=27}
Person{name='绿地(实例工厂)',age=29}

Bean注入属性有哪几种方式

  • setter注入
  • 构造器注入
  • 注解注入
  • Java Config注入
setter注入

在UserService使用的时候依赖于UserDao,目前UserDao是空的,要想运行时通过setter方法注入的话,我们需要在injection.xml中
<context:component-scan base-package=”com.imooc.spring”/>




前面的name=”userDAO”对应着UserService.java中的userDAO属性 private UserDAO userDAO
后面的ref=”userDAO”对应着当前文件中的bean id = “userDAO”

com/example/text/injection/UserDAO.java
package com.example.text.injection;

public interface UserDAO {
    public void insert();
}
com/example/text/injection/UserDAOImpl.java
package com.example.text.injection;

import org.springframework.stereotype.Repository;

//@Repository("userDAO") ★注解方式注入
public class UserDAOImpl implements UserDAO{
    public UserDAOImpl(){
        System.out.println(this + "已创建");
    }
    public void insert() {
        System.out.println(this + ":正在调用UserDAOImpl.insert()");
    }
}
com/example/text/injection/UserService.java
package com.example.text.injection;

import org.springframework.stereotype.Service;

import javax.annotation.Resource;

//@Service("userService") ★注解方式注入
//spring需要动态将userDao注入到UserService上
public class UserService {
//  @Resource(name="userDAO")  注解方式注入 可以把原始的bean都注释掉
    private UserDAO userDAO;

    public UserService() {
        System.out.println(this + "已创建");
    }
/* 注解方式这些可以抛弃 并且摒弃了xml的那些bean 只需要增加个扫描的注解方式注入
    //通过构造函数注入
    public UserService(UserDAO userDAO) {
        this.userDAO = userDAO;
        System.out.println(this + ":正在调用构造函数注入,UserService(" + userDAO + ")");
    }

    //通过Setter方法注入
    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
        System.out.println(this + ":正在调用Setter方法注入,setUserDAO(" + userDAO + ")");
    }
*/

    public void createUser(){
        System.out.println(this + ":正在调用UserService.createUser()");
        userDAO.insert();
    }
}
com/example/text/InjectionRunner.java
package com.example.text;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class InjectionRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:injection.xml");
        UserService userService = (UserService)ctx.getBean("userService");
        userService.createUser();
    }
}

==============================================================
UserDAOImpl@531be3c5对象已创建
UserService@5034c75a对象已创建
UserService@5034c75a:正在调用Setter方法注入,setUserDAO
UserService@5034c75a:正在调用UserService.createUser()
UserDAOImpl@531be3c5:正在调用UserDAOImpl.insert()
C:\Users\Pluminary\Desktop\text\src\main\resources\injection.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.imooc.spring"/> //★扫描注解方式注入
    <bean id="userDAO" class="com.imooc.spring.injection.UserDAOImpl"/>
    <bean id="userService" class="com.imooc.spring.injection.UserService">
        <property name="userDAO" ref="userDAO"/> //★这个是setter注入
        <constructor-arg name="userDAO" ref="userDAO"/> //★这个是构造函数注入
    </bean>
</beans>
Java config注入
Appconfig.java 
[配置文件 在原有代码之外的东西 优点:不用破坏原始代码去达到效果 比xml优点在于java可以及时检查]
package com.imooc.spring.injection;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig { //类似创建对象的方法
    @Bean(name="userDAO")
    public UserDAO userDAO(){
        return new UserDAOImpl();
    }

    @Bean(name="userService")
    public UserService userService(){
        UserService userService = new UserService();
        userService.setUserDAO(this.userDAO());
        return userService;
    }
}

Spring常见面试问题

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="userDAO" class="com.imooc.spring.scope.UserDAOImpl"/>
    <bean id="userService" class="com.imooc.spring.scope.UserService" scope="prototype" lazy-init="false">  
//scope是控制对象的数量是单例(singleton)还是多例(prototype)[每一次创建getbean创建对象]
//还有两个不常见的值 request[同一个对象是同一个请求 不同对象ICO容器创建不同对象] 和 
//session[调用多个getbean是同一个对象 不同session是不同对象] [SpringMVC时用]
        //lazy-init="true" 延迟初始化 需要的时候才会加载 若getBean被注释则不会加载
        <property name="userDAO" ref="userDAO"/>
    </bean>
</beans>
ScopeRunner.java
package com.imooc.spring.scope;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ScopeRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:scope.xml");
        UserService userService1 = (UserService)ctx.getBean("userService");
        UserService userService2 = (UserService)ctx.getBean("userService");
    }
}
UserDAO.java   UserDAOImpl.java   UserService.java 与上方相同
两个注解

@Autowired[第三方] 和 @Resource[官方推荐]

@Resource(name=”userDAO”) //设置了则进行精准匹配
private UserDAO userDAO;//没设置则优先将名字userDAO在beanId内查找, 再没有就按照UserDAO类型在IOC容器中查找

AOP有几种通知类型

在不修改原始程序的前提下使用通知来对程序进行扩展
  • 前置通知[Before]
  • 返回通知[After returning]
  • 异常通知[After throwing]
  • 后置通知[After]
  • 环绕通知[Around] (统统实现)
SampleAspect.java
package com.imooc.spring.aop;

import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

//切面类
public class SampleAspect {
    
    //1.前置通知
    public void doBefore(JoinPoint jp){
        String clzName = jp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = jp.getSignature().getName();//即将要执行的方法
        Object args = jp.getArgs();
        System.out.println("【前置通知】"  + clzName + "." + method );
    }
    
    //2.后置通知
    public void doAfter(JoinPoint jp){
        String clzName = jp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = jp.getSignature().getName();//即将要执行的方法
        System.out.println("【后置通知】"  + clzName + "." + method );

    }
    
    //3.返回通知
    public void doAfterReturning(JoinPoint jp , Object ret){
        System.out.println("【返回后通知】" + ret);
        
    }
    //4.异常通知
    public void doAfterThrowing(JoinPoint jp , Throwable t){
        System.out.println("【异常通知】" + t.getMessage());
    }

    //5.环绕通知
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
        String clzName = pjp.getTarget().getClass().getName();
        //getTarget() 获取即将要执行的对象
        String method = pjp.getSignature().getName();//即将要执行的方法
        Object args = pjp.getArgs();
        System.out.println("【前置通知】"  + clzName + "." + method );
        Object ret = null;
        try {
            ret = pjp.proceed();//执行目标方法
            System.out.println("【返回后通知】" + ret);
        } catch (Throwable t) {
            // TODO Auto-generated catch block
            System.out.println("【异常通知】" + t.getMessage());
            throw t;
        }finally{
            System.out.println("【后置通知】"  + clzName + "." + method + "()");
        }
        return ret;
    }
    
}
UserService.java
package com.imooc.spring.aop;

import com.imooc.spring.scope.UserDAO;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
//@Transactional(propagation = Propagation.REQUIRED , rollbackFor = Exception.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED , readOnly = true )
public class UserService {

    public String createUser(){
        //打开事务
        System.out.println(this + ":正在调用UserService.createUser()");
        //提交事务
        //catch块中回滚事务 - RuntimeException
        return "success";
    }
}
AOPRunner.java
package com.imooc.spring.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPRunner {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:aop.xml");
        UserService userService1 = (UserService)ctx.getBean("userService");
        userService1.createUser();
    }
}
aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd
 http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 定义类被Spring IOC容器管理 -->
    <bean name="sampleAspect" class="com.imooc.spring.aop.SampleAspect"></bean>
    <bean name="userService" class="com.imooc.spring.aop.UserService"></bean>
    <!-- 配置Spring AOP -->
    <aop:config>
        <!-- 配置过程中引用切面类  sampleAspect是切面 扩展-->
        <aop:aspect ref="sampleAspect">
            <!-- PointCut(切点) 限制了切面应用的范围 ↓↓aop作用了哪些方法上↓↓-->
            <aop:pointcut id="samplePC" expression="execution(* com.imooc.spring.aop.*Service.create*(..))" />
            <!-- 定义通知 -->
            <!-- 前置通知 method=对应着SampleAspect.java中的doBefore-->
            <aop:before pointcut-ref="samplePC" method="doBefore"/>
            <!-- 后置通知 -->
            <aop:after pointcut-ref="samplePC" method="doAfter"/>
            <!-- 返回后通知,注意:返回后通知需要增加retruning属性
            指向doAfterReturning的名为ret的参数,使用ret参数获取方法的返回值 -->
            <aop:after-returning method="doAfterReturning" pointcut-ref="samplePC" returning="ret"/> 
<!--3.ret是返回通知里面的参数 public void doAfterReturning(JoinPoint jp , Object ret)-->
             <!-- 异常通知 -->
<!--public void doAfterThrowing(JoinPoint jp , Throwable t)中的Throwable的参数是目标方法所抛出的异常  throwing="t"相关噢~ -->
            <aop:after-throwing method="doAfterThrowing" pointcut-ref="samplePC" throwing="t"/>
            -->
            <!-- ★★★★ 环绕通知 ★★★★ -->
            <aop:around method="doAround" pointcut-ref="samplePC"/>
        </aop:aspect>
    </aop:config>
</beans>

请介绍Spring的声明式事务

Spring的声明式事务是Spring框架提供的一种管理事务的方法,它允许开发者通过声明的方式管理事务,而不是通过编写繁琐的事务管理代码。这种方式使得事务管理更加解耦,业务逻辑代码不需要直接处理事务的开启、提交、回滚等操作,从而使得代码更加简洁和易于维护。

  • 在执行方法前自动开启的事务
  • 声明式事务式指利用AOP自动提交、回滚数据库事务
  • 声明式事务式规则进入方法打开事务,成功提交,运行时异常回滚
  • @Transactional是声明式事务的注解 放在类上则所有方法执行此事务 放在方法上则单独方法执行此事务
  • propagation = 确定方法是否启动事务[Propagation.REQUIRED]执行的方法自动使用事务
    rollbackFor = Exception.class在什么时机进行回滚 readOnly = true 方法只读
  • 通过程序打开或关闭事务属于编程式事务
UserService.java
package com.imooc.spring.aop;

import com.imooc.spring.scope.UserDAO;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
//@Transactional(propagation = Propagation.REQUIRED , rollbackFor = Exception.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED , readOnly = true )
public class UserService {
    public String createUser(){
        //进入方法 → 打开事务
        System.out.println(this + ":正在调用UserService.createUser()");
        //方法执行成功 → 提交事务
        
        //方法抛出运行时异常 → 回滚事务
        //catch块中回滚事务 - RuntimeException[及其子类会自动回滚]
        return "success";
    }
}

使用SpringMVC实现REST风格

  • REST(表述性状态传递)以URL表示要访问的资源
  • GET/POST/PUT/DELETE对应查询、新增、更新、删除操作[浏览器不支持PUT和DELETE操作]
    [用GET对应查询操作(写)、用POST对应新增、更新、删除操作(写)]
  • REST风格只响应的数据,通常是以JSON形式体现

以下是一些常用的HTTP方法及其在REST中的用途:

  • GET:检索资源的表示。
  • POST:创建新的资源或子资源。
  • PUT:更新或替换资源。
  • DELETE:删除资源。

REST风格的API通常被称为RESTful API,它们通过使用标准的HTTP方法、状态代码和URI来提供一种简单、一致和可预测的方式来访问和操作网络资源。RESTful API的设计通常遵循一些最佳实践,比如使用名词而不是动词来命名资源,使用HTTP状态码来传达操作的结果等。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>interview-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
web/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
interview-springmvc\src\main\resources\applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 配置扫描的包 -->
    <context:component-scan base-package="com.imooc.interview"/>
    <!-- 注册HandlerMapper、HandlerAdapter两个映射类 开启注解模式-->
    <mvc:annotation-driven/>
    <!-- 访问静态资源 -->
    <mvc:default-servlet-handler/>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/> //所有URL不分层级和格式
            <bean class="com.imooc.interview.rest.MyInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>
Employee.java
package com.imooc.interview.rest;

public class Employee {
    private String name;
    private int age;
    private String department;

    public Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }
}
RestfulController.java
package com.imooc.interview.rest;

import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
@Controller //在IOC容器初始化的情况下自动扫描这个类进行加载并且通知IOC容器 这个是一个MVC中的控制类
//@RestController
public class RestfulController { //{XXX}是路径变量 随时产生变化的 注解@PathVariable("DEPT")
    @GetMapping("/emp/list/{DEPT}") //响应GET请求 前面的是响应URL输入这个的时候响应下面的方法
    @ResponseBody
    public String list(@PathVariable("DEPT") String department) {
        List<Employee> list = new ArrayList<Employee>();
        if (department.equals("RESEARCH")) {
            list.add(new Employee("JAMES", 38, "RESEARCH"));
            list.add(new Employee("ANDY", 23, "RESEARCH"));
            list.add(new Employee("SMITH", 31, "RESEARCH"));
        }
        return JSON.toJSONString(list);//只返回纯粹的JSON数据 只返回页面名称
        //如何只返回数据不跳转页面呢? 用 @RespondBoday 直接将返回的数据输出到客户端
//但是可以简化使用 @RestController 所有的可以直接不用写 @RespondBoday
    }
}//前端使用AJAX技术接收数据

请说明SpringMVC拦截器的作用 [底层就是AOP面向切面编程技术]

  • SpringMVC拦截器用于对控制器方法进行前置、后置处理
  • 拦截器的底层实现技术是AOP(面向切面编程)
  • 拦截器必须实现HandlerInterceptor接口
★★ 上一个代码通用 ★★
interview-springmvc\src\main\java\com\imooc\interview\rest\MyInterceptor.java
package com.imooc.interview.rest;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.logging.Logger;

public class MyInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入 preHandle 方法..." + request.getRequestURL().toString());
        return true;
    }
 

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("进入 postHandle 方法..." + request.getRequestURL().toString());
    }
 

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("进入 afterCompletion 方法..." + request.getRequestURL().toString());
    }
}
====================================================================================
查看上面的applicationContext.xml
<bean>
<!-- 访问静态资源 -->
    <mvc:default-servlet-handler/>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/> //所有URL不分层级和格式
            <bean class="com.imooc.interview.rest.MyInterceptor"/> 
            //对上面这个类进行处理和拦截
        </mvc:interceptor>
    </mvc:interceptors>
</bean>

SpringMVC的执行流程

说明Mybatis的开发过程

案例沿用之前的scott.sql表

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
             
    <groupId>com.imooc</groupId>
    <artifactId>interview-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>
</project>
mybatis.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <!-- 配置数据库连接信息 -->
            <dataSource type="POOLED"> <!--数据库连接池的方式-->
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/scott" />
                <property name="username" value="root" />
                <property name="password" value="root" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/emp.xml"></mapper>
    </mappers>
</configuration>
Employee.java
package com.imooc.interview.mybatis.entity;

import java.io.Serializable;
import java.util.Date;

public class Employee implements Serializable {
    private Integer empno;
    private String ename;
    private String job;
    private Integer mgr;
    private Date hiredate;
    private Float sal;
    private Float comm;
    private Integer deptno;
}Getter + Setter
用于查询的配置文件 emp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 命名空间是个虚拟的类 -->
<mapper namespace="com.imooc.interview.mybatis.mapper.EmpMapper"> 
    <cache size="512" eviction="FIFO" flushInterval="60000" readOnly="true"/>
    <select id="findAll" resultType="com.imooc.interview.mybatis.entity.Employee" useCache="false">
        select * from emp
    </select>
<!-- 类型是int 把传入的int代入到#{value}中 -->
    <select id="findById" parameterType="int" 
            resultType="com.imooc.interview.mybatis.entity.Employee" useCache="true">
        select * from emp where empno = #{value}
<!--查询数据 根据数据自动创建Employee对象 根据字段名把值一一的设置到属性中-->
<!--记得写完以后再mybatis.xml中注册 通知mybatis 有个<mapper resource="mapper/emp.xml">-->
<!--至此mybatis的配置全部完成 -->
    </select>
</mapper>
MybatisRunner.java
package com.imooc.interview.mybatis.entity;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MybatisRunner {
    public static void main(String[] args) {
        //mybatis的配置文件 核心配置文件↓
        String resource = "mybatis.xml";
        InputStream is = MybatisRunner.class.getClassLoader().getResourceAsStream(resource);
        //构建sqlSession的工厂(对Mybatis进行解析)  build(is)是初始化
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
//com.imooc.interview.mybatis.mapper.EmpMapper前面是命名空间
        //.findById对应着select的Id
        String statement = "com.imooc.interview.mybatis.mapper.EmpMapper.findById";
        SqlSession session = sessionFactory.openSession();
//selectOne获取唯一的查询结果  之前定义了要传入整数int
        Employee emp1 = session.selectOne(statement, 7566);
        System.out.println(emp1);
        session.close();
    }
}

Mybatis的缓存机制

把一些不太常变动的数据保存在内存中或高速存储器中,从而降低查询时间提高查询效率
  • Mybatis存在两级缓存

    • 一级缓存与SqlSession会话绑定,默认开启 [巨大的HashMap] [默认开启]

    • 二级缓存是应用全局缓存,所有SqlSession共享 [分布式数据库等]

//一级缓存验证结果
Employee emp1 = session.selectOne(statement, 7566);
Employee emp2 = session.selectOne(statement, 7566);
System.out.println(emp1);
System.out.println(emp2);
======================打印结果========================
com.imooc.interview.mybatis.entity.Employee@6a41eaa2
com.imooc.interview.mybatis.entity.Employee@6a41eaa2
从缓存中将这个7566提出来,所以对象是一样的
随着session的关闭 → session.close(); 一级缓存也随着消失; 输出的对象结果就不相同了
//二级缓存验证结果  让7566全局缓存 默认是不开启的 需要在mapper中开启 → emp.xml
//在缓存中最大容纳512个对象 缓存多余清除策略[FIFO先进先出算法 LRU访问最少的对象清除] 
//flushInterval=>时间间隔定时清理缓存[毫秒数 每60秒]
<cache size="512" eviction="FIFO" flushInterval="60000" readOnly="true"/>
//若对全部查找 则不适合使用缓存 因为每次都变化 这样就可以设置useCache="false"
<select id="findAll" resultType="com.imooc.interview.mybatis.entity.Employee" useCache="false">
//对数据进行新增、修改、删除也会对缓存进行强制更新 上面设置 readOnly="true" 保存在缓存中的数据都是只读的
//二级缓存要对工具类Employee进行接口的实现
public class Employee implements Serializable {...}

Spring、SpringMVC与SpringBoot的区别

  • Spring是所有应用的基础,提供了IOC与AOP特性实现对对象轻松的管理
  • SpringMVC是Spring的子项目用于提供Web支持 替代传统的Servlet [提高兼容性和可维护性]
  • SpringBoot是Spring体系的敏捷开发框架,提高了开发效率 [对Spring整个体系都有良好的支撑]
    • Spring Framework是整个Spring的框架基础
    • Spring Cloud对分布式架构与微服务提供了良好的支持
    • Spring Data简化关系型数据库 非关系型数据库 大数据源
    • Spring Batch提供高效率的批处理任务
    • Spring Security超级牛逼的安全框架 登录验证之类的
    • Spring Amqp消息队列进行支持 统一的接口进行适配
    • Spring Mobile手机开发特性

SpringBoot面试题

  • Spring Initializr是创建SpringBoot Project的唯一方法吗?

    否!也可以用Maven进行手动创建,也可以官网创建

  • SpringBoot支持几种配置文件?

    只有两种

    一种
    server.port=80
    debug=false
    server.servlet.context-path=/myspringboot
    logging.file=d:/logs/msb.log
    spring.mvc.date-format=yyyy-MM-dd
    
    二种 [按层级进行区分 yml 对程序维护有很大帮助]
    debug:false
    server:
      port:8000
      servlet:
        context-path:/
    spring:
      mvc:
        date-format:yyyy-MM-dd
    
  • 请列举至少五个SpringBoot中配置选项

    配置名称 默认值 描述
    server.port 8080 端口号
    server.servlet.context-path / 设置应用上下文
    logging.file 日志文件输出路径
    logging.level info 最低日志输出级别
    debug false 开启/关闭调试模式
    spring.datasource.* 与数据库相关的设置

Maven的构建生命周期

命令 用途
mvn archetype:generate 创建Maven工程构建
mvn validate 验证工程结构
mvn compile 编译源代码
mvn test 执行测试用例
mvn package 项目打包
mvn install 安装至本地仓库
mvn deploy 发布至远程仓库

请简述一下Java反射的作用和原理

作用:
Java反射(Reflection)允许程序在运行时检查和操作类、接口、字段和方法的信息
① 动态获取类的信息:获取类的名称、父类、实现的接口等;获取类的构造方法、字段和方法的信息
② 动态创建和访问对象:创建类的实例;访问和修改对象的字段;调用对象的方法
③ 动态调用方法:调用私有方法、静态方法、实例方法等;传递参数和处理返回值
④ 动态代理:创建动态代理类,实现接口和动态代理

原理:
① 获取Class对象:
Ⅰ. 通过类名获取:Class.forName(“类的路径”);
Ⅱ. 通过类字面常量获取:类名.class;
Ⅲ. 通过对象获取:对象名.getClass();
② 获取类的信息:
Ⅰ. 获取类名:类名.getName;
Ⅱ. 获取父名:类名.getSuperclass();
Ⅲ. 获取实现的接口:类名.getInterfaces();
Ⅳ. 获取构造方法:类名.getConstructors();
Ⅴ. 获取字段:类名.getFields();
Ⅵ. 获取方法:类名.getMethods();

优点和缺点
优点:能够运行时动态获取类的实例,提高灵活性;与动态编译结合
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析
解决办法→通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时有缓存会快很多








;;;;;;;;;;;;;;;;;;;;洪哥面试题分隔符;;;;;;;;;;;;;;;;;;;;;;

线程池的执行流程大致如下:

线程池:ThreadPoolExecutor
一开始new的时候没有 是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略

  • 丢弃任务抛出异常
  • 丢弃任务不抛弃异常
  • 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
  • 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
    • 问题:想继续异步且不丢弃任务怎么办?
    • 把这个业务先存到别的地方 ↓↓↓
  • 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞

怎么确定核心线程数和最大线程数

核心线程数
  • CPU密集型任务:如果任务是CPU密集型的,即任务主要是进行计算而不是等待I/O操作,核心线程数通常设置为CPU核心数加1。这样可以确保CPU在忙于计算的同时,还有额外的线程来处理可能出现的临时高峰。【纯内存计算 不涉及到网络计算和io计算】
    • 八个核 创建十个cpu 没意义 因为最多并发只是8,建议保持一致或者+1,减少加入队列和创建队列的开销
    • 先把其当成io密集 因为层级不一样 不断压测去逼近最理想值
  • I/O密集型任务:对于I/O密集型任务,由于线程在等待I/O操作时会阻塞,因此可以设置更多的核心线程数。一个常用的经验法则是核心线程数设置为CPU核心数的两倍。【线程数越多越好】【压测无限逼近取最合适的线程数】
最大线程数

需要一开始创建好线程等着访问来,如果 核心=最大,此时没有临时线程

创建线程有几种方式(必会)

1.继承Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类
2.实现Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3.实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4.使用线程池创建(使用java.util.concurrent.Executor接口)

  • 想获得线程池里的返回结果用什么?execute + submit
  • 线程有哪些状态? java线程有哪些状态?
  • 线程池有哪些状态?

线程池哪些类型?通过JUC[包]的executes可以创建这四个类型的线程池

问题:为什么阿里巴巴不推荐JUC?有可能会出现OOM、资源浪费

  • 单线程线程池
  • 可缓存线程池/定长
  • 变长的线程池
  • 定时任务的线程池

java 线程池创建时核心参数(高薪常问)

核心线程池大小、线程池创建线程的最大个数(核心+非核心[临时线程])、临时线程存活时间、时间单位、阻塞队列、线程工厂(指定线程池创建线程的命名)、拒绝策略
线程工厂可以设置创建的属性
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]

阻塞队列常用的队列

  1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
    • 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
    • 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
    • new的时候不用指定长度
  2. LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为Integer.MAX_VALUE,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】
    • 读中间的慢 读头尾快
    • 新增元素不涉及到数组的迁移
    • 一般情况下高并发推荐使用,因为队列高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】
    • new的时候可指定长度是最大链表的长度
    • 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM

线程池的应用要有实际的业务场景

  • 异步任务处理:将任务提交到线程池异步执行,而不阻塞主线程

你单独部署过项目吗?

前端打包ng配置文件
git所有人都用 需要拉分支 maven打包后端 包放到远程服务器 java -jar 启动!【不应该有人去做】
有专门工具去流水线制作 → Jenkins是一个开源的自动化服务器,它可以帮助您实现自动化构建、测试和部署项目 JenKins + docker 做自动化部署
部署是建立本地的项目再推到服务器

你的期望薪资?

我目前的薪资是8000,考虑到我即将承担的职责和我的职业发展,我期望的薪资是在现有基础上有所提升,大约在8000到10000之间。当然,我对整体的薪酬包[包括福利、奖金和职业发展机会]也很感兴趣。薪资是如何构成的,包括固定工资、奖金、股权、福利等。

get请求和post请求的区别

get请求
  • 请求指定的资源。使用GET的目的是获取数据,
  • 数据在URL中传输,通过将数据附在URL之后,以查询字符串的形式出现
  • 由于数据在URL中可见,因此安全性较低,敏感数据不应通过过GET请求发送
  • URL长度限制通常在2000个字符左右,这意味着GET请求能够传输的数据有限
  • 可以被缓存,也会被浏览器保存在历史记录中
  • 常用于信息查询、数据检索等操作.
post请求
  • 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
  • 数据存储在请求体(HTTP消息主体)中,不会密在URL上
  • 数据不会出现在URL中,相对更安全,适合传输敏感信息。
  • 理论上没有大小限制,适用于传输大量数据.
  • 不会被缓存,且不会保存在浏觉器历史记录中
  • 常用于数据提交、表单提交等操作

请求行:请求类型 请求方法 url http版本1.1 老式1.0不支持长连接
请求头:key value
常见请求头: 请求数据类型,restful基于json
Content-Type:上传文件不用application 要用 multipart/form-data”
Host:指定请求的服务器的域名和端口号。
User-Agent:包含发出请求的用户代理软件信息,通常包括浏览器类型和版本
请求体:get请求可以有请求体
响应:响应行 响应体 状态码 描述
常见响应头Content-Type:返回数据的格式 Content-Length:响应体的长度,以字节为单位

post请求也可以用问号的形式拼接到浏览器 也可以用路径参数

很大区别:get一般放在url后面 会展示url和后面参数 会暴露传参隐私 登录接口用post来做 有密码敏感信息
表单、密码、长数据用post 不过怕黑客抓包 相对来说安全
get请求后面传参的大小限制 理论上没有限制 只是浏览器厂商会有限制
get用来查询 post新增提交表单

是否上传过图片

阿里云是最后存储的
完整的上传图片应该:
前端要配合(表单 post提交) Content-Type:上传文件不用application 要用 multipart/form-data” 同一个请求能边上传图片和文本数据
后端的操作:传到后端controller接收,有一个类multipart 专门接收二进制数据 图片视频等,有很多api → get input stream封装util 调用upload上传。中小型企业都用阿里云oss 因为要考虑容灾 地震 容易数据丢失,要考虑备份→集群,服务器有物理硬件上限(要有运维成本),文件维护很专业交给专业的人。阿里云的机房在深圳,广州的人访问会比哈尔滨的更快。光纤受物理限制 越长越有损耗。大型运营商在全国各地都有机房,可以智能判断比如哈尔滨的就去访问黑龙江服务器。CSDN内容分发(收费)

前端传过来的图片怎么设置图片大小 有没有什么办法?

思考:为什么后端要限制前端图片传的大小?
大图片 + 多人上传,首先后端要读到jvm内存再二进制流给到阿里云,同时并发有可能超出OM的java内存大小
springboot yml加文件上传大小配置

spring:
  servlet:
    multipart:
      max-file-size: 10MB # 单个文件的最大大小
      max-request-size: 20MB # 整个请求的最大大小,包括多个文件的总和

你在里面主要负责哪方面的工作?

我之前负责后端开发 也会参与一部分设计工作
开发完会协助测试 和前端进行联调
和组长一起进行测试
和前后端的逻辑基本上都是可以的

上家公司的离职原因,薪资多少,薪资结构

不要说一些面试官能挑刺的理由
发展前景?表明上家公司不好
太想进步?表名上家公司提供的技术不好 自己技术不好
在上家公司我学习了很多 成长了很多,个人发展原因 ,想要涨薪

// 来自AI的答案 仅供参考
我在上家公司学到了很多,但我觉得为了我的职业发展,我需要寻找一个能够提供更多成长机会和挑战的职位。我想要在[技能/领域]上进一步深耕,而贵公司的职位看起来非常符合我的职业规划;我在上家公司的年薪大约在6000到7000之间;我的薪资结构主要包括基本工资、每年两次的绩效奖金、股票期权以及一些标准福利,比如健康保险、退休金计划等。此外,公司还提供了一些额外的福利,比如灵活的工作时间和远程工作的机会
简历公司

上家公司如果问工作不好找 为什么不先找到再离职
我在这一块想好好准备面试 但是上班的时间不好分配 我想专心去找工作
上家工作繁忙抽不出时间去准备 所以我想多多准备
异地公司 → 万能理由:现在面的公司在哪家里人就在哪[地理位置要接近 精确到哪个城市] 异地很多都线下不方便先离职专心准备

薪资多少

现在期望12k 上家最好保证**20%-30%**区间→8-9-10k(参考城市不同)

薪资结构

基本工资(七八成)+绩效工资(20%-30%) 有公司先扣除 有的当月发
A 120% S 150%-200% C 80%

你对上家公司的看法

不能贬低 要说优点 学习成长了很多 同事和领导都很照顾我

什么时候能入职?

三个工作日 到 一周之间

你离职了 现在有多少个offer了?

不能说一个都没有

  • 我已经有2个offer 但是一定要表达对当前公司的期待 经过我的了解 我更喜欢贵公司的发展和文化
  • 我也是刚刚开始找工作…

你可以接受加班吗

(必须完全接受全部加班 先拿到offer再说)

Controller和RestController的区别

@RestController = @Controller + @ResponseBody

@Controller如果要返回JSON/XML等格式的数据给客户端,必须显式的使用@ResponseBody注解将返回的对象转换为HTTP响应体内容。
@RestController 专门为构建RESTful Web服务设计的控制器。它简化了创建API的过程,因为所有方法默认都会将返回值直接写入HTTP响应体中作为JSON或XML格式的数据。

@Controller可以声明一个类为一个bean 控制器用
@ResponseBody 具体方法和类都可以 不是包装类和字符 都可以自动转成json数据格式 更符合restful风格

在yaml文件中定义了一些参数,该怎么调用

  • 使用 @Value 注解,这是最直接的方式,适用于简单的属性注入。是bean的注解 用${key}还可以用#
    • ${}:用于注入外部配置文件的值。它告诉Spring需要从环境变量、属性文件、系统属性等地方查找相应的值。
    • #{}:用于执行SpEL(Spring Expression Language,Spring表达式语言)表达式。它允许你在注入值时执行一些简单的计算或逻辑。
    • 如果在多个类里引用 配置多 杂乱 可以写个配置类写一堆的属性 提供get set方法 配置类.get获取到配置
  • 使用 @ConfigurationProperties 注解,通常会指定一个前缀prefix),这个前缀用于指定配置文件中哪些属性应该被绑定到这个 Bean 上。
    @ConfigurationProperties(prefix = "prefix")

IOC和DI有了解过吗,它们的好处是什么

它们的目的是为了解耦
IOC(控制反转)是Spring的两大核心之一,DI(依赖注入)
IOC把控制权交给spring容器
对象创建好之后 之间会有依赖关系 DI因此而生
实现方式:DI通常有四种实现方式

  • 属性注入 注解注入

    • @Autowired 是 Spring 提供的注解,用于自动装配 Bean。它可以用于字段、构造函数、方法或设置器上。当 Spring 容器启动时,它会自动查找并注入匹配的 Bean。
      • 偶尔有不影响程序运行的报错?写spring技术人员是根据jdk写,怕别人不用。
    • @Resource 是 Java 的注解[JDK的],用于依赖注入,它也可以用于字段、方法或设置器上。与 @Autowired 不同的是,@Resource 默认通过名称进行匹配,如果未指定名称,则尝试通过类型进行匹配。
    • 两者区别
      • @Autowired 先根据属性类型 去容器里面找 如果找不到 再根据**属性名称[字段]**去找 如果实在找不到就会报错 [@Autowired永远不会放弃你的 尽其所能去帮你找]
      • @Resource 先根据属性名称去找 要么找不到 要么找到一个 找到就去注入 如果找不到 可以再根据属性类型去找 [类型找不到 或者 找到多个 也会报错]
  • 构造函数注入 [默认生成空参构造方法 若写有参构造原来无参会被覆盖 参数根据类型去找和@Autowired类型一样 可以写多个构造方法 如果去多个构造方法重载会报错 怎么办?加个@Autowired[属性,构造方法,参数]都可加 不可多个方法都加@Autowired 反射会触发构造方法 @Bean => new ]

  • Set方法注入[原生spring 用xml去定义才有 SpringBoot没有这个注入 ]

  • 普通方法注入

测试过程有没有出现反复的困扰?

客户需求频繁更改
测试用例没有覆盖到
开发和测试环境未协调

太复杂的改动要先报备技术经理、项目经理

测试:自测 单元测试 专业人员

公司使用哪些技术?

后端:Redis RabbitMQ 搜索引擎 微服务常用组件 远程调用 统一网关 Springboot Springcloud MybatisPlus

项目有多少个成员?

2前 8后 1测 1运维 1项目经理(小公司约13人左右) 要具体人数
自研公司?外包?

自研公司

  • 创业型自研公司:通常员工人数在10-50人之间,初期可能更少,只有几人到十几人。
  • 成熟自研公司:员工人数可能从几十人到几百人甚至更多。

外包公司

  • 小型外包公司:员工人数可能在10-50人之间。
  • 中型外包公司:员工人数可能在50-200人之间。
  • 大型外包公司:员工人数可能超过200人。

HashMap底层原理

底层数据结构

jdk1.8之前底层结构是数组+链表(key+value) 数据结构通用的[键值对+哈希表的数据结构]
jdk1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时且数组长度大于64时,将链表转化为红黑树,以减少搜索时间。扩容 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。后期使用map获取值时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
底层的地址运算出来 如果地址不一样计算出来的hashcode不一样,hashcode一般是数字[整数(±或0)] 通过key进行hashcode运算 对数组长度取模 eg:任何整数去取模10 可以定位到value可以放在哪个桶下面

hashcode本身不同的对象算出来的hashcde值是相同的怎么办呢?
两个value不可能放在同一个桶 这就是hash冲突 如果数组长度是8 算出来一个hashcode值是8 和 16 此时取模余数相同这样的情况也是相同。所以此时应该用拉链法[小葡萄串],即使桶一样 可以用指针一个个指,此时的链表是单项链表[linkedlist才是双向链表]。拉链法的解决哈希冲突。
在1.8后指出如果同一个桶的葡萄串太多了,此时要拿出数据,时间复杂度就是O(n),如果没有很多就是O(1)数组的长度。红黑树的引入是解决链表过长的问题。
红黑树是树形的高级数据结构 时间复杂度O(logn)
二叉树在某些情况下会退化成链表 右子树永远比根节点大
红黑树会旋转自平衡[局部旋转达到平衡] 超过多少层会旋转 不至于退化成链表。

扩容机制

new的初始化 数组为空
当第一次put的时候才不会为空 为16
扩容因子为什么是0.75?hashmap不仅仅java 其他语言也有这种数据结构 但扩容因子可能不同,是通过大量的数学概率统计出的最好最平衡的值。达到12的时候会扩容成2倍
new一个hashmap可以指定数组长度为7 此时数组长度是8【hashmap的长度永远是2的幂次方 比你传入的长度永远大 且 是2的幂次方】 为什么2的幂次方?因为1.7要数组取模 怎么打这个符号 shift+5 => %,1.8之后用了位运算,>>2 <<2 让你的取模运算更快。如果出现hash冲突会拉链 当它的数组长度大于64 并且 链表长度大于8时,当链表长度小于等于6临界值会变回来【为什么是6?避免频繁切换(离8太近) 链表 ←→ 红黑树[消耗性能]】

链表1.7之前是头插法 会产生一条首尾相接的死循环【并发情况[但是hashmap线程不安全不会用在并发,要用ConcurrentHashMap]一起put 且 同一个桶】
1.8之后是尾插法,并发情况下不会出现cpu飙高,

HashSet底层数据结构

底层是包装了一个hashmap,无序 key不允许重复 value可重复
HashSet单列无序不重复的 key就是那个元素 value就是new了一个无意义的object对象

ArrayList和LinkedList不是线程安全的 用什么?

  • Vector 读写都加锁。
  • CopyOnWriteArrayList读不加锁 写加锁

ConcurrentHashMap能存null吗?不允许使用 null 作为键,但是允许使用 null 作为值。
HashMap:null默认放在第一个桶下面 下标写死为0

Hashcode相同equals一定相同吗?

hashCode() 相同不一定意味着 equals() 相同,但 equals() 相同则 hashCode() 必须相同。

  • equals() 方法用于判断两个对象是否逻辑上相等。
  • hashCode() 方法用于返回对象的哈希码,这个哈希码通常用于哈希表的快速查找。

key可以放复合对象,要注意要重写 hashcode()和equals() 如果不重写 new了的两个对象有可能会相同
"重地""通话" 。计算hashcode会比equals更快,一个对象new出来后hashcode已经计算出来了。equals要比较每个对象值,所以先判断hashcode 再判断equals 重写:@Override 用属性里面的hashcode,user里面包含了复杂对象order 此时order也要重写。包装类已经重写了hashcode,要整个对象返回true才为正确的,要层层递进去判断。hashmap重写复杂对象就一定要重写那俩个 ∵ 是比较对象里的属性值
list 有序可重复单列
map 双列key不能重复value可重复 treemap是有序的
set 单列不重复无序 hashset 无序 treeset 有序

== 值 + 地址值
equals 是对象属性值是否一 一相等

HashMap是线程安全的吗

不安全的,可以使用ConcurrentHashMap、Collections.synchronizedMap()、HashTable
线程安全:多线程对同一个数据进行增删改是否受到影响
怎么办?

  • 加锁

    • synchronized
    • ReentrantLock

    加锁为什么能解决线程安全问题?线程访问资源的先后顺序
    多线程访问同一个数据 => 多个线程访问同一个数据
    秒杀 =>[思想] 1w个人买 对 100个库存进行扣减,只搞100个线程 把100个库存分成10份 其中每份有10个

    初始化库存: 创建一个共享的库存计数器,初始值为100。
    创建线程: 创建100个线程,每个线程在启动时分配到一个特定的库存分片。
    扣减库存: 每个线程尝试扣减其分配到的库存分片中的一个商品。扣减操作必须是原子的,以确保线程安全。
    同步机制: 使用适当的同步机制(如synchronized关键字、ReentrantLock等)来保护库存扣减操作,防止并发问题。
    库存检查: 在扣减前,线程需要检查当前分片是否有剩余库存。如果没有,则线程可以终止或进行其他处理。
    

    hashtable不管读写都会用synchronized加锁,并发一起来读都加锁 没必要,所以用了ConcurrentHashMap读不加锁 写加锁。
    随着时间的推移,Hashtable 已经被认为是遗留代码,现代Java代码更倾向于使用 HashMap(非线程安全)或 ConcurrentHashMap(线程安全)。

红黑树查询效率高的原因

红黑树是一种自平衡的二叉搜索树,它通过特定的规则来保持树的平衡,这些规则确保了树的高度大约是log(n)
自平衡 局部旋转

hashMap时间复杂度

  • O(1):不涉及到拉链
  • O(n):拉链不是树
  • O(logn):桶为红黑树

这个测试是你做的吗,还是你们团队去做的?

像自测的话是我自己去做的(测试用例、apifox),然后交给专业测试人员

团队是怎么协作的?

像我们团队的话,有使用禅道来做我们的文档管理,需求管理和需求的变更控制,和工作的一个统计报表,大部分的协作任务都可以在禅道上完成,代码这一块我们是使用git来做一个代码管理和协作的。

linux查看每个文件夹下的文件大小的命令

ls -lh

linux查看文件夹大小的命令

du -sh
du -sh –all 隐藏
du -sh ./* : 单独列出各子项占用的容量

linux查看进程的命令,动态查看一个文件的最后100行的命令

ps -ef
ps aux [查看所有用户的进程(包括其他用户的进程):]
ps -ef | grep mysql

动态:tail -n 100 -f xxx 【-f → follow】
静态:tail -n 100 xxx

常用的Linux命令

rm = romove

用于删除文件和目录
rm [-rf] name
-r(recursive递归):将目录及目录中所有文件(目录)逐一删除,即递归删除
-f(force):无需确认,直接删除

rmdir = remove directory

它用于删除空目录。如果目录不为空,即目录中包含文件或其他子目录,rmdir 命令将无法删除该目录

pwd = print working directory

打印出当前工作目录的绝对路径。当你需要知道你在文件系统中的当前位置时,这个命令非常有用

cp = copy

复制文件和目录。这个命令可以用来创建文件的副本或将文件从一个位置移动到另一个位置。
cp [-r] source dest
-r (recursive递归):如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件

mv = move

为文件或目录改名、或将文件或目录移动到其他位置【移动 重命名 修改】

grep

用于搜索文本数据,特别是使用正则表达式来匹配指定的模式
查看特定进程的详细信息,例如进程名为 mysql
ps -ef | grep mysql

tar [tape archive]

用于打包多个文件和目录到一个归档文件中,或者从归档文件中提取文件

cd

切换路径

vim

编辑文件

cat

查看文件[head]

如何查日志

查看/var/log/user.log文件,并且想要跟踪用户 name:pcy 的活动

tail -f /var/log/user.log | grep “pcy”
高级专用使用awk 可以用正则等一些逻辑操作去获取日志

内建函数

awk 非常强大,可以用于执行复杂的文本分析和报告生成,awk 有许多内建函数,如 length()toupper()tolower() 等。

bash

复制

awk '{print toupper($0)}' filename  # 将所有内容转换为大写

条件语句

bash

复制

awk '{if ($1 > 100) print$1}' filename

循环

bash

复制

awk '{for (i=1; i<=NF; i++) print $i}' filename

数组

bash

复制

awk '{count[$1]++} END {for (word in count) print word, count[word]}' filename

你们接口是如何让前端调用的

我们会在设计阶段提前设计好给前端 并行开发 前后端联调[本地ip端口告诉前端]

接口文档怎么定下来的

根据页面原型、需求设计接口文档[后端自己写],绝大部分后端看原型的出参入参 无太大需求和前端商量。【前端组件库】[若修改返回结构的时候] [按照数据结构修改] 需要听前端意见

前端调用后端用的是什么请求方式

WebSocket【基于长连接通讯】
HTTP

前端开发中,以下是一些常见的使用场景:

  • 获取数据:使用GET请求。
  • 提交表单或数据:使用POST请求。
  • 更新资源:使用PUT或PATCH请求。
  • 删除资源:使用DELETE请求。

前端可以通过多种方式发起这些请求,例如:

  • 使用HTML表单(通常用于GET和POST请求)。
  • 使用JavaScript的XMLHttpRequest对象或者更现代的fetch API来发起各种类型的HTTP请求。
  • 使用各种前端框架和库(如React, Angular, Vue.js)中提供的封装好的HTTP服务。

SpringBoot主要的一些注解?都有哪些,以及主要作用

SpringBoot:
@SpringBootApplication [见↓↓]
@ConfigurationProperties:注解用于将外部配置(如来自properties文件、YAML文件或环境变量)绑定到JavaBean上。它的作用是将配置文件中的属性映射到JavaBean的属性上,这样就可以在应用程序中使用这些配置属性。
@SpringBootTest:用于测试 Spring Boot 应用,提供测试环境的支持
@EnableConfigurationProperties:启用对配置属性的支持,允许将配置文件中的属性注入到 bean 中。


Spring:
@Component 
@ComponentScan 
@Conditional 
@SpringBootApplication 是一个组合注解,它结合了以下三个注解的功能:
1. @SpringBootConfiguration: 表示这是一个Spring Boot配置类,它本质上是一个@Configuration注解,用于定义配置类,可以包含多个@Bean注解的方法。
2. @EnableAutoConfiguration: 告诉Spring Boot基于类路径设置、其他bean和各种属性设置来添加bean。例如,如果你添加了spring-webmvc和thymeleaf的依赖,这个注解就会自动配置你的应用程序为一个web应用程序。
3. @ComponentScan: 告诉Spring在包及其子包下扫描注解定义的组件(如@Component, @Service, @Repository等)。

aop在项目中有没有使用?aop使用的一些注解及其功能

一定要描述项目场景,web使用aop打印操作日志、使用aop做数据脱敏(150***8786)
过滤器是Servlet技术的一部分,它是Java EE规范的一部分
拦截器是Spring MVC框架的一部分,用于在处理HTTP请求时拦截控制器方法调用。
AOP底层是动态代理设计模式,在理论上效果在一定程度上相同
过滤器拦截器一般拦截某个web的前后,在controller执行前后
AOP是万物皆可拦截、甚至接口和类都可以切,可以增强controller、service、mapper……

定义一个切面类 @Aspect 声明为切面类 + @Component
定义切点 @Pointcut 声明切点表达式

eg:@AfterReturning(pointcut = “execution(public String com.example.yourpackage.Controller.*(..))”, returning = “result”)

通知
  • 前置 @Before
  • 后置 @After
  • 返回 @AfterReturning
  • 异常 @AfterThrowing
  • 环绕 @Around

你在公司里负责的内容

想在controller访问完之后,想在aop实现之后再进行操作

UserThreadLocal 在执行完之后要 remove 出去,抛异常也会执行
@After 不管有无异常都会执行
@Around 结合try…catch…finally 里也可以达到同样效果

@After:这个注解用于定义一个通知(Advice),它在目标方法执行之后执行,无论目标方法执行的结果如何(成功或异常)。

@Aspect
@Component
public class AroundFinallyAspect {

    // 定义切点
    @Pointcut("execution(* com.example.yourpackage.controller..*(..))")
    public void controllerMethods() {
    }

    // 环绕通知
    @Around("controllerMethods()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            // 在目标方法执行之前执行
            result = joinPoint.proceed(); // 执行目标方法
            // 在目标方法成功执行之后执行
        } catch (Throwable e) {
            // 在目标方法抛出异常时执行
            throw e; // 可以选择处理异常或者重新抛出
        } finally {
            // 无论目标方法是否成功执行或者是否抛出异常,这里的代码都会执行
            performFinallyAction();
        }
        return result;
    }

    private void performFinallyAction() {
        // 在这里放置最终要执行的代码
    }
}

--------------------------------------------------------------------------------
// 后置通知
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        // 在目标方法执行之后执行的逻辑
    }

    // 返回后通知
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        // 在目标方法成功执行并返回结果后执行的逻辑
    }

在第一个例子中,你直接在@After注解中指定了切点表达式,因此不需要额外的pointcut属性。

对于@AfterReturning注解,它不仅需要在目标方法执行之后执行通知,还需要访问目标方法的返回值。因此,@AfterReturning注解有一个额外的pointcut属性,用于指定切点表达式。此外,@AfterReturning注解还有一个returning属性,用于指定一个参数名,该参数将接收目标方法的返回值

@AfterReturning:在方法执行后返回结果后执行通知。【如果有异常不会处理】

你们这个项目怎么技术选型的

我进到项目中很多已经确认下来的 一般由组长确定了

那你自己怎么想的?[开放性问题]

选xxx技术 网上资料/备书 比较多可以参考[用的人多]
学习成本[框架厉害但上手复杂不好用]
社区的活跃度[官网持续更新版本 框架会不断发展]

你处于后端的什么位置

初级 中级 高级
中级 骨干开发位置[协助组长完成]
中高级 完成设计类

你对你的职业规划是什么

讲实际的话
想成为高级开发/某个领域的专家
提前了解公司领域,有备而来

java基本类型

image-20241222141950707

short 可以占两个字节 可以用16位
int 可以占四个字节 -21亿 ~ 21亿
long 可以占八个字节 天文数字
float 可以占四个字节 0.2F/f
double 可以占八个字节 0.2D/d

float double尽量不要进行运算 ,在Java中进行金钱运算时,应当特别注意浮点数的精度问题,因为浮点数(如floatdouble)在表示某些数值时可能会丢失精度,这对于需要精确计算的金融计算来说是非常不合适的。

1.金钱转成分 向下取整
2.BigDecimal

ASCII码占1个字节 → Unicode字符占2个字节【有些汉字存不了】→ UTF-8占1-3个字节[灵活可变]
在我们性能中一般走Unicode编码性能更高一点 在网络中/存入磁盘Unicode转成ASCII码

jdk?之后 jdk开始存储大量英文和数字 String类也作了更新 不是基于基本数据类型 而是基于byte数组

在Java的早期版本中(例如JDK 1.4及之前版本),String类内部确实使用char数组来存储字符串数据。每个char在Java中占用16位(2个字节),这意味着不管存储的是英文字符还是数字,每个字符都会占用2个字节的内存空间。

从JDK 5开始,Java平台引入了一些变化,但String类的内部表示仍然基于char数组。直到JDK 6和JDK 7,String类的内部表示并没有改为基于byte数组。

真正发生变化的是在JDK 9中,String类内部表示从char数组转变为byte数组加上一个编码标识(coder),这种改变是为了更有效地存储只有ASCII字符的字符串。ASCII字符只需要一个字节来表示,因此使用byte数组可以节省内存空间。当字符串包含Unicode字符时,String类可能会使用更多的编码方式,例如LATIN1或UTF-16。

String是基础类型吗

不是,是java.lang下的类

String 在 Java 中并不是基础类型,而是一个引用类型。因为 String 是一个类,所以它是引用类型,意味着当我们声明一个 String 变量时,你实际上是指向一个 String 对象的引用

String 的特性
不可变性:String 对象一旦创建就不能被修改。任何改变 String 内容的操作都会创建一个新的 String 对象。
线程安全:由于 String 的不可变性,它们是线程安全的,可以自由地在多个线程之间共享。
字符串池:为了提高性能和减少内存使用,Java 为 String 提供了字符串常量池(String Pool)。当创建一个新字符串时,如果字符串池中已经存在相同内容的字符串,则会返回池中的实例,而不是创建新的对象。

java集合中list和set的区别?

都是接口 某个实现类

单链 有顺序 可重复 有索引[有下标]
单链 不可重复 无索引[无下标] 不能说是无序 因为TreeSet有序 HashSet就是无序的

做了几年开发呢? 实际几个项目?

三年[初中级] → 四~五个项目

你觉得敲代码最重要的是什么?

理解需求前期设计工作[数据库、接口 → 流程图(思路清晰)]、编码阶段[考虑方法封装、注释、考虑代码后期和维护性(设计模式 → 可维护性+扩展性)]、编码风格[阿里巴巴规范]

你的项目有上线吗? 多少人进行开发? 你主要负责后端吗?

有,介绍一下项目组成结构,是的[再问再回答]

SpringBoot的自动装配原理[启动过程中的一部分]SpringBoot启动原理&&如何内嵌外部原件

Spring Boot的自动装配原理是基于Spring框架的IoC(控制反转)和DI(依赖注入)的核心概念,并结合了一系列的约定和条件注解来实现配置类的自动加载和Bean的自动注册

自定义Starter
<!-- Maven项目的依赖示例 -->
<dependency>
    <groupId>com.xxx</groupId>
    <artifactId>xxx-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

三大优点:依赖Maven特性[依赖传递] 自动配置 内嵌Tomcat
Spring Boot的自动装配原理是
src/main/resources/META-INF目录下创建spring.factories文件,添加自动配置类的全限定名
我们可以在-info定义spring.factories位于META-INF目录下,Spring Boot使用它来发现和加载自动配置类。

配置类扫描: 通过@SpringBootApplication注解,Spring Boot会触发对@EnableAutoConfiguration注解的处理,该注解会查找spring.factories文件中定义的自动配置类。

Maven里面写test类 用configuration声明 写很多的test类 但是我可以自己写test类然后调不同的方法 应该怎么办?@Conditional[Spring的注解] → 做成非常灵活的 如果没有就用自己写的

Spring里面的事务传播行为

在Spring框架中,事务传播行为定义了事务方法之间的调用关系,即一个事务方法被另一个事务方法调用时,事务应该如何传播。

  1. REQUIRED(默认值) required
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. SUPPORTS supports
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  3. MANDATORY mandatory
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. REQUIRES_NEW requires_new
    • 创建一个新的事务,如果当前存在事务,则挂起当前事务。
  5. NOT_SUPPORTED not_supported
    • 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。
  6. NEVER never
    • 以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED nested
    • 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为类似于REQUIRED

REQUIRED:通常用于方法需要在一个事务中运行,但如果已经有一个事务在运行,那么它应该加入这个事务。
SUPPORTS:用于方法不需要事务上下文,但如果已经在一个事务中,它也可以在这个事务中运行。
MANDATORY:用于方法必须在事务中运行,如果没有事务,则会抛出异常。
REQUIRES_NEW:用于方法必须在自己的新事务中运行,即使当前已经有一个事务在运行。
[一般适用于不管有没有抛出异常 都要记录某些操作日志 不能在同一个类里底层是动态代理]
[如果a()和b()方法在同一个类中,并且a()直接调用b(),那么Spring的事务代理无法拦截这个内部调用,因此b()的REQUIRES_NEW事务传播行为不会生效。这是因为内部方法调用不会通过代理,而是直接在同一个对象实例上调用。]

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

@Service
public class MyService {

    @Autowired
    private MyService self; // 注入自身代理实例

    public void methodA() {
        // ... 业务逻辑 ...

        self.methodB(); // 通过代理实例调用,事务注解将生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ... 业务逻辑 ...
    }
}

NOT_SUPPORTED:用于方法不应该在事务中运行,如果有一个事务在运行,它将被挂起。
NEVER:用于方法绝对不应该在事务中运行,如果有一个事务在运行,将抛出异常。
NESTED:用于方法应该在嵌套事务中运行,嵌套事务可以独立于外部事务进行提交或回滚

用过Spring的事务吗

一组数据库的增删改操作
声明式事务管理:这是Spring推荐的用法,它通过使用注解(如@Transactional)或基于XML的配置来声明事务边界。底层基于AOP实现动态代理增强方法
编程式事务管理:允许你通过编程的方式直接管理事务,通常使用TransactionTemplate或者直接使用底层的PlatformTransactionManager

你熟悉的技术栈有哪些,用了哪些?

SpringBoot Vue Git Maven MyBatis……

解决难点的决策有和用户沟通的吗?

有过沟通 让他们了解一下我们的方案

万一用户听不懂怎么办?

我会用一些更加直白通俗的语言让用户理解我们的方案

用户不接受这个方案怎么办?

我们可以提供不止一个方案 或者 提供他提的方案 综合一下各种方案告诉其优缺点(站在我们的专业角度意见)和风险 让客户明知 让其选择

通常一般怎么学习的?最近在研究什么技术?

想面试的目的 要給公司带来一些好的
一般以公司的实际项目中为切入点去学习会更有效率

学习一个新的技术大概要多久?

1-2天 首先看官网 这个技术是解决哪些 看我们的项目需要哪些技术切入点能引用 然后去专门针对这个技术功能点去学习快速上手的接口文档

redis为什么这么快?

  • 主数据基于内存操作
  • Redis是单线程[操作数据的线程],避免上下文的频繁切换整个redis不是就一个线程
  • 底层基于C语言实现 得益于底层良好的数据结构[]
  • 基于非阻塞的IO提升IO读写性能,NIO,BIO,AIO…
Java四大杀手

集合数据结构 jvm 并发编程 网络IO

redis的数据类型以及使用场景分别是什么

  • String:存储对象信息(转JSON)、
  • List:链表,查询记录的缓存、列表,朋友圈,微博,队列数据结构
  • Hash:获取局部属性,小key不能设置过期时间
  • Set:无序不可重复的,收藏,点赞,社交场景,聚合计算(∩∪差集)
  • Zset:排序场景,排行榜,姓名排序

分布式锁都可以用。Redisson是Redis的儿子,底层为Hash

redis数据过期策略

  • 惰性删除:键过期时不会立即删除,当访问该键时判断是否过期,如果过期就删除
  • 定时删除:设置键的过期时间,当键过期时,立即删除
高薪冲刺 → 定时删除详细策略

要扫描所有的定期任务删除 有策略可以设置阈值

啥时候离职的?半个月太长了

刚刚离职 也是刚刚开始投

主要工作职责

主要负责后端工作,协助测试,运维上线

你对前端有了解过吗?

有了解过,如HTML、CSS、JavaScript、框架[Vue、Element]等 可以很快的上手
我主要专长在于后端开发,可以学习和了解更多的前端知识

各种淘汰策略介绍

Redis提供了8种淘汰策略,可以分成两大类:

1、针对所有键的策略:对所有键进行选择和淘汰。

2、仅针对有过期时间的键的策略:只在设置了过期时间的键中选择淘汰对象。

以下具体策略:
可以区分为两类:[有设置过期时间的key 不管你有没有设置过期时间]

1. noeviction【默认】
  • 描述:达到内存限制时,不再执行删除操作,直接拒绝所有写入请求(包括插入和更新)。[可以读 但是拒绝写请求]
  • 适用场景希望数据永不丢失的场景,但需要保证内存充足,否则会导致写入操作失败。
2. allkeys-lru(最近最少使用)
  • 描述:在所有的键中使用 LRU算法,删除最近最少使用的键。
  • 适用场景:适合缓存场景,保留频繁访问的键,逐出很少被访问的键。
3. allkeys-lfu(最少使用频率)【电商】
  • 描述:在所有键中使用 LFU 算法,删除使用频率最低的键。
  • 适用场景:适用于需根据使用频率进行淘汰的场景,更关注访问次数而非访问时间。
4. volatile-lru(最近最少使用)
  • 描述:仅对设置了过期时间的键使用 LRU 算法。
  • 适用场景:适合缓存一些有过期时间的数据,希望根据访问频率来进行内存管理的场景。
5. volatile-lfu(最少使用频率)【电商】
  • 描述:仅对设置了过期时间的键使用 LFU算法。
  • 适用场景:同 volatile-lru,但更关注使用频率
6. allkeys-random
  • 描述:在所有键中随机选择删除某个键。
  • 适用场景:适用于缓存数据访问频率没有明显差异的情况。
7. volatile-random
  • 描述:在所有设置了过期时间的键中随机选择删除某个键。
  • 适用场景:适合缓存带有过期时间的数据,删除哪个数据不重要的场景。
8. volatile-ttl
  • 描述:在设置了过期时间的键中,优先删除剩余生存时间(TTL)较短的键。
  • 适用场景:适合希望优先清理即将过期的数据的场景。

缓存三兄弟(穿透、击穿、雪崩)

一般在读缓存的时候出现的问题。思路:产生的原因 + 解决的方案

==缓存穿透==:用户或前端查询到一个在数据库中不存在的数据,先查redis再走数据库。对数据库压力会很大。关系型数据库是性能的瓶颈 希望把高数量都挡在数据库前面。查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)

  • 解决方案一缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null} 【空字符串】没有Null的数据类型下一次读取直接把空串返回
    优点:简单
    缺点:消耗内存,可能会发生不一致的问题

如果一直模拟一个不同的不存在的key 这时候就要用到布隆过滤器

  • 解决方案二布隆过滤器 (拦截不存在的数据)
    [商品读多写少上缓存,要把商品数据写到布隆过滤器中,以商品的id独一无二计算hashcode,用布隆过滤器。取模数组落到桶内 会把0置为1]
    有很多个二进制数组每个二进制数组用不同的hash算法进行计算此时落到的桶就不一样
    作用:读的时候 前端传id 之前怎么写进去就怎么拿出来。[位运算(与)速度很快 把多个数组的数据拿出来与运算如果都是1 则这个数据可能存在再查一遍缓存 若不存在直接return返回] 布隆过滤器说你不存在 一定不存在,说你存在 则可能存在[哈希冲突]

    ★ 项目上线很久了 商品早就下架不卖了 这时候会发生什么问题

    这时候布隆过滤器还有之前的痕迹,需要把那些1设置为0。
    布隆过滤器 不支持对某个的1设置0 → 因为有哈希冲突我不知道这个1曾经是誰设置的
    支持将整个都置为0,之后可以搞个定时任务
    布隆过滤器具体实现:Redis、Redission亲儿子、1cache、咖啡因(Caffeine提供了一种非常高效且易于使用的缓存解决方案,它支持多种缓存过期策略)、Guava谷歌

    ★ 以前没设置过且上架过 后面加了布隆,后面要把之前所有数据重新搞进去 怎么解决存量数据

    写一个定时任务

    ★ 场景:工商银行统计每天的用户日活量[上线就算] 要查询某个人连续七天签到 怎么查(用位图)用户量太多了

    搞一个二进制数组,10亿长度的数组,每个数组是一个bit = 10亿个位,一个字节1/bit=8个位,综合计算后大概消耗119MB的空间每天。用用户id去hash 如果用户登录将0置为1有单独的位图结构,统计时间就可以拿日期 往前面数 拿某个id去取模得到桶 找前七个,去进行与运算,连续为1就达到了重复连续七天前端。否则非连续七天。

    在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回

    **bitmap(位图)巨大的二进制数组**:相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1

    布隆过滤器作用:可以用于检索一个元素是否在集合中

    • 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
    • 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

    存在误判率:数组越小 误判率越大 【要数组足够大 误判率就小】

    bloomFilter.tryInit(size, 0.05) //误判率5%
    

==缓存击穿==:给某一个热点key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿微博[鹿晗+关晓彤]【并发同一时间访问】

  • 解决方案一互斥锁【数据强一致性 性能差 (银行)】[控制一个个来访问的次数]

    AQS、ReentrantLock是进程级别的互斥锁,因为有数据在节点1或节点2,分布式锁是在不同场景都可以锁也可以控制访问顺序。

    以商品id作为key 先redis开始查缓存 判断是否为空 不为空直接return后解锁,空就先加锁 去数据库查完备份一份redis后解锁。被锁的其他线程在外面等待。

    ★ 100个人访问同一个商品,只有一个抢到锁,剩下的99个人也要查redis缓存和数据库。

    方案:**双重缓存校验** 先查缓存 查不到加锁 再查缓存 查不到再去数据库 查完后看是否备份后解锁冷代码

    1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁

    1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中

  • 解决方案二逻辑过期[ 不设置过期时间 ] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】

    在数据库一条数据里面添加一个 “expire”: 153213455

    1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据

    ​ 【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
    1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据

==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力

  • 解决方案一:给不同的key的TTL(过期时间)添加随机值不在同一时间过期
  • 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
  • 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
  • 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
★ redis宕机的时候 再RedisTemplate.set()后会报错 但是现在mysql还可以访问 应该怎么办?

try catch 在里面继续再去查mysql数据库

降级代码:对于读操作,如果Redis缓存失效,可以直接从MySQL数据库读取数据。
public boolean setData(String key, String value) {
    try {
        // 尝试将数据设置到Redis
        redisTemplate.opsForValue().set(key, value);
        return true;
    } catch (Exception e) {
        // 日志记录Redis错误
        log.error("Redis is down, failing over to MySQL", e);
        
        // Redis设置失败,降级到MySQL
        return setDataToMySQL(key, value);
    }
}

private boolean setDataToMySQL(String key, String value) {
    // 这里实现将数据写入MySQL的逻辑
    // 例如:
    // mySqlTemplate.update("INSERT INTO cache (key, value) VALUES (?, ?)", key, value);
    // 注意:这里的mySqlTemplate和SQL语句需要根据实际情况调整
    return true; // 假设写入成功
}
如果公司对要求更高,需要限流降级、熔断

同一时间设置QPS为100 超过的返回友好提示[商品太火爆啦,请稍后再试]

你有自己部署过环境吗

公司里面用 Jenkins + docker 测试环境我们部署 生产环境是组长部署

未来1-3年规划

将具体一点,从业务技术上提升自己的深度和广度达到高级工程师

你平时做笔记吗

有做笔记 Xmind + Markdown
因为我觉得无论从网上的还是别人请教的不经历我的消化都不是我的东西
我还是会将这些知识点总结起来变成自己的知识

什么是动态代理?&& 动态代理有哪些,他们之间的区别?

代理是一种设计模式 用来增强目标的逻辑 与被增强的并没有太大关系装饰者模式

在程序运行期间才会产生代理类加载到我们jvm中

  • JDK动态代理是基于接口实现来实现增强

    [txt文本 把目标增强类 作为接口本身就是接口 实现过来写成源码 源文件 再用jdk工具把源码编译成class字节码 再用类加载器把class加载到jvm中]

  • CGLIB动态代理是基于继承目标类并覆写其方法来实现

    [ASN字节码机制直接生成class 直接加载到内存中]性能较高,速度更快。因为直接生成class

要调用某个方法 CGLIB性能高 是通过反射来实现的 老版本的jdk的反射性能较低。如今在调用方法的性能上差距不大

区别

  • JDK动态代理要求目标类必须实现一个或多个接口,而CGLIB没有这个要求。
  • JDK动态代理生成的代理类是接口的实现,而CGLIB生成的代理类是目标类的子类。
  • 性能上,CGLIB通常比JDK动态代理更快,因为它直接操作字节码生成新的类。

什么样的代码是静态代理?

发生在我们写代码的过程中 在编译阶段产生了代理类
静态代理是指代理类在编译时就已经确定,通常由程序员手动编写

你用过Linux吗?

是的,我在工作中经常使用Linux操作系统。我熟悉Linux的基本命令
基础的命令:xxx【查看之前笔记】

你工作的时候有需求文档吗?

有的,有一些简单的需求是没有的[沟通成本太高了]
稍微复杂的需求会有需求文档,我会根据需求文档来理解项目需求,并进行系统设计和开发。

你有什么需要了解的?不要难为面试官,不问技术栈

我想了解一下贵公司的业务是什么…好的那我这块已经没有什么想了解的了 感谢面试官
HR:想了解一下贵公司的上班时间…
我没有什么想了解的,来之前有了解过贵公司

平时用注解创建的bean是单例的还是多例的?

默认情况下,通过注解(如@Component、@Service、@Repository、@Bean等)创建的Bean是单例的。如果需要创建多例Bean,可以在注解上添加@Scope(“prototype”)来指定。

SQL语句的执行顺序,为什么顺序是这样排的,这样的顺序有什么优势或者好处?

FROM -> Join -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> limit
这样的顺序是为了优化查询性能。首先确定数据来源(FROM),然后筛选出满足条件的数据(WHERE),接着进行分组(GROUP BY),在分组的基础上进行进一步筛选(HAVING),然后选择需要的数据(SELECT),最后对结果进行排序(ORDER BY)。这样的顺序可以减少中间结果集的大小,提高查询效率。

书写顺序

select -> from -> join -> on -> where -> group by -> having -> order by -> limit

线上项目发生死锁如何去解决? 我暂时没遇到过→分布式事务上去答

死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行

  1. 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
  2. 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
  3. 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
  4. 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

在实际操作中,以下是一些打破死锁的具体方法:

  • 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
  • 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
  • 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
  • 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
  • 线程中断:允许系统或其他线程中断正在等待资源的线程。
  • 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。

MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

如果你遇到(新的)技术栈,怎么去解决?

【return Previous.notes(NowDay);】

如果你在实际开发中遇到问题,你怎么去解决,怎么去沟通?

首先尝试自己解决问题,通过搜索引擎、官方文档、Debug等。
尽可能不让这个问题不出现风险 实在解决不了就向上反馈 寻求帮助 请教上司领导或同事
平常和项目经理进行沟通 如果需求评审有些不理解还是会及时沟通 不清楚的一定要及时明确

对于加班情况怎么看?

为了确保项目进度和团队利益,加班是可以接受的。

多线程怎么保证线程之间的安全

加锁 不让多线程抢夺资源

互斥锁、读写锁、线程局部存储(ThreadLocal每个线程独享自己变量)

mybatis中${}和#{}的区别,哪个更好? 为什么?

  1. ${}(字符串替换):
    • ${}会将参数直接替换到SQL语句中,不进行任何转义处理。
    • 它适用于动态SQL中的表名或列名,或者在SQL语句中需要使用特定数据库函数的情况。
    • 使用${}时,如果参数是用户输入的,那么可能会引发SQL注入攻击,因为它不会对参数进行转义。
  2. #{}(预处理语句参数):
    • #{}会创建预处理语句(prepared statement)的参数占位符,并在设置参数时进行适当的转义处理。
    • 它适用于大部分情况,特别是当参数是用户输入时,可以有效防止SQL注入攻击。
    • MyBatis会根据参数的类型自动选择setStringsetIntsetDate等预处理语句方法。
  3. 在大多数情况下,#{}是更好的选择,因为它提供了以下优势:
    • 安全性#{}可以防止SQL注入攻击,因为它会自动转义参数。
    • 类型处理:MyBatis会根据参数的实际类型来设置预处理语句的参数,这减少了类型错误的可能性。
    • 可读性和维护性:使用#{}可以使SQL语句更加清晰,因为它清楚地标识了参数的位置。

    然而,在某些特定的场景下,如需要动态地指定表名或列名时,${}是必要的,因为预处理语句

说一下内连接和外连接的区别

左外连接(Left Outer Join)

  • 定义:左外连接返回左表中的所有行,即使在右表中没有匹配的行。对于左表中没有匹配的行,结果集中的右表部分将包含NULL。

右外连接(Right Outer Join)

  • 定义:右外连接返回右表中的所有行,即使在左表中没有匹配的行。对于右表中没有匹配的行,结果集中的左表部分将包含NULL。

全外连接(Full Outer Join)

  • 定义:全外连接返回左表和右表中的所有行。当某行在另一个表中没有匹配时,结果集中的相应部分将包含NULL。

自我介绍

xxx

你觉得学习我们这些技术最重要的是什么?

首先要清楚这个技术是解决什么领域的问题,学习技术很多方面都是用来服务业务的,结合实际业务来学习技术融合性会更强

技术栈有些不同,有没有想过换方向发展?

没问题的 因为技术是相通的 可以去学新技术

1.概述

ThreadLocal(定义全局静态变量 项目中共用)是Java中的一个线程局部变量工具类,它提供了一种在多线程环境下,每个线程都可以独立访问自己的变量副本的机制。ThreadLocal中存储的数据对于每个线程来说都是独立的,互不干扰。

2. 使用场景

ThreadLocal适用于以下场景:

  • 在多线程环境下,需要保持线程安全性的数据访问。
  • 需要在多个方法之间共享数据,但又不希望使用传递参数的方式。
    • 在传递登录用户id是非常方便且适用

以后获取用户id不用再解析token了,线程拿仅仅拿当前线程的数据 每个登录的用户都有自己的threadlocal数据

ThreadLocal并不是一个Thread,而是Thread的局部变量【可以存储数据】
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。**ThreadLocal实现一个线程内传递数据**[就不用一个个参数往后传递了]
注意:客户端发送的每次请求,后端的tomcat服务器都会分配一个单独的线程来处理请求

  • 一个请求是一个线程[意义:在第一个线程里面使用ThreadLocal存储用户Id 在后面controller或service中就可以取出来用户id]
  • 第二个请求就是另一个线程 线程池用完第一个放回线程池 也有可能把上一个线程接着给它用

postHandle 只有在正确调用返回才会引用 如果抛出异常则不会使用
afterCompletion 无论怎样最后都要运行

ThreadLocal

3.1 创建ThreadLocal对象

首先,我们需要创建一个ThreadLocal对象来存储线程局部变量。可以使用ThreadLocal的默认构造函数创建一个新的实例。【给每个线程拷贝一份 synchn + Lock锁】

ThreadLocal<String> threadLocal = new ThreadLocal<>();
3.2 设置线程局部变量的值

使用set()方法可以设置当前线程的局部变量的值。

threadLocal.set("value");
3.3 获取线程局部变量的值

使用get()方法可以获取当前线程的局部变量的值。

String value = threadLocal.get();
3.4 清除线程局部变量的值

使用remove()方法可以清除当前线程的局部变量的值,建议在整个请求使用完一定要执行remove清除数据,不然可能会发生内存泄漏问题。

threadLocal.remove();
下面是一个简单的示例代码,演示了如何使用ThreadLocal。
public class ThreadLocalTest {

    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("itheima");
            getData("t1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("itcast");
            getData("t2");
        }, "t1");

        t1.start();
        t2.start();

    }

    private static void getData(String threadName){
        Object data = THREAD_LOCAL.get();
        System.out.println(threadName+"-"+data);
    }
}

运行以上代码,输出结果为:

t1-itheima
t2-itcast

在任意位置都可以调用Threadlocal,线程隔离互不影响,解决了线程安全问题:[每个线程存一份 线程不共享]

用mybatis+建造者模式 一定要在类里面加 一定要具有有参和无参构造缺一不可 否则mybatis数据封装无法映射
@AllArgsConstructor
@NoArgsConstructor
@Builder

字符和字符串类型的区别

字符是基本数据类型 没有null 用单引号
字符串是引用数据类型[一个类] 用双引号

image-20250102142357092

操作字符串的工具类是什么

apache的common提供的String Utils工具类,hutool也有String Utils

局部变量和成员变量的区别

成员变量是在jvm的堆
局部变量是在jvm的栈
基本数据类型的引用类型 类在堆
基本数据类型的成员变量在堆 非静态在堆 静态变量在方法区
引用类型 无论静态还是非静态成员变量都在堆区

局部变量是在方法或代码块内部声明的变量,其作用域仅限于声明它的方法或代码块
局部变量不能被static修饰
局部变量必须被初始化才能使用

成员变量是在类内部声明的变量,其作用域是整个类
成员变量可以被static修饰
成员变量有默认值

你编写完代码,写完这个功能后,会进行什么操作呢

进行代码审查,检查代码是否符合编码规范和设计要求。

  • 进行单元测试,确保代码的功能正确无误。
  • 进行代码优化,提高代码的性能和可维护性。
  • 与团队成员进行代码合并,确保代码的集成。
  • 编写文档,记录功能实现和代码变更。

上一家公司的薪资是多少?期望薪资是多少?上一家还有什么其他的福利吗?

期望薪资:
了解过广东这边的市场 我想换工作想涨薪10~20%
节假日会发放礼品和福利

可以接受低代码平台吗?

可以接受
低代码平台:类若依

具体说说Java面向对象

Java面向对象是一种编程范式,它将现实世界的事物抽象成程序中的对象。Java面向对象的主要特征包括:

  • 封装:将对象的属性和行为封装在一起,对外只暴露必要的接口,隐藏内部实现细节。
  • 继承:允许子类继承父类的属性和行为,实现代码的复用。
  • 多态:同一个接口可以有多个不同的实现,通过对象的类型和方法的调用,实现不同的功能。

== 和 equals 的区别

  • ==:比较基本数据类型时,比较的是值;比较引用数据类型时,比较的是对象的内存地址。
  • equals:是Object类的一个方法,默认比较的是对象的内存地址。但在很多类中(如String、Integer等),equals方法被重写,用于比较对象的内容是否相等。
    没重写 就是 == 比较对象地址。重写过的话就比较对象的值。

有没有做过权限控制,整个系统的权限

有过 SpringSecurity

能具体说一下权限控制怎么做?

使用RBAC模型 不是把用户关联资源 而是中间利用角色间接关联
用户+角色+资源+用户角色中间表+角色资源中间表多对多

SpringSecurity 具体怎么实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息。

线程池有哪些状态,这些状态是怎么进行转换的

线程池有以下几种状态:

  • RUNNING:线程池正常运行,可以接受新的任务和处理任务队列中的任务。
  • SHUTDOWN:线程池不再接受新的任务,但会处理任务队列中的任务。
  • STOP:线程池不再接受新的任务,也不处理任务队列中的任务,并且会中断正在执行的任务。
  • TIDYING:所有任务都已终止,线程池即将关闭。
  • TERMINATED:线程池已关闭。

状态转换过程如下:

  • RUNNING -> SHUTDOWN:调用shutdown()方法。
  • RUNNING -> STOP:调用shutdownNow()方法。
  • SHUTDOWN -> TIDYING:当线程池和任务队列都为空时。
  • STOP -> TIDYING:当线程池为空时。
  • TIDYING -> TERMINATED:当terminated()钩子方法执行完成后。

说一下怎么使用多线程?

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,并将实现类传递给Thread对象。

  • 实现Callable接口,实现call()方法

  • 使用Executor框架,如ExecutorService和ThreadPoolExecutor来管理线程池。

操作系统上的线程有多少种状态[5]?Java线程有多少种状态[6]?

  • 新建(New):创建后尚未启动的线程处于这个状态。new Thread
  • 可运行(Runnable):包括运行(Running)和就绪(Ready)状态,线程正在执行或等待CPU调度。
  • 阻塞(Blocked):线程因为等待某些资源或锁而被阻塞。notify可以唤醒阻塞状态 睡眠完会自动唤醒
  • 等待(Waiting):线程等待其他线程执行特定操作(如通知)。
  • 计时等待(Timed Waiting):线程在一定时间内等待另一个线程的通知。
  • 终止(Terminated):线程执行完成或因异常而终止。
怎么把线程杀死 终止

stop()方法[暴力方法] interrupt()方法[优雅关闭线程] 正常回收

乐观锁和悲观锁的区别

乐观锁:读多写少 线程执行时间相差较大 并发不太激烈

悲观锁:写多读少 线程执行时间相差不大 竞争激烈 并发锁多

加锁的时机不一样,
悲观锁:没改数据的时候先加锁 比较明显利用底层操作系统api实现
乐观锁:在改数据的时候才加锁 依靠底层的硬件

java层面synchronized ReentrantLock

数据库层面
悲观锁:select for update是mysql的的实现
乐观锁:JUC Java Util Concurrent)是Java并发工具包

SELECT ... FOR UPDATE:这个语句在读取记录时会锁定这些记录,直到事务提交或回滚。其他的事务不能更新这些锁定的记录,这是悲观锁的一个典型实现

乐观锁要读取目前旧的值再将新设置的值以及旧的值比较 如果相同 就把新的值更新 如果不相同 就把旧的值重新提取 因为在这期间有人读取了这个数据跟我之前不一样(底层api 要调用两个 一个旧的值 一个新的值)。一般乐观锁是结合自旋 类于while(true)直到读到为止 要设计次数后再报错

要更新数据库某个值 把旧的值读出来 想更新银行里的余额
这是典型的ABA问题要用时间戳自增版本号去做

Stream流的使用及常用API

Stream是Java 8中引入的一种新特性,用于简化数据处理和操作。它可以用来解决集合循环遍历处理的问题。在此之前用循环来代替

基础Stream操作

  • stream(): 为集合创建串行流。
  • parallelStream(): 为集合创建并行流。
  • forEach: 对每个元素执行操作。
  • map: 将每个元素映射到对应的结果。
  • filter: 过滤出满足条件的元素。
  • limit: 限制流的大小。
  • skip: 跳过流中的前n个元素。
  • sorted: 对流进行排序。

终端操作

  • collect: 将流转换为其他形式,比如列表、集合或Map。
  • reduce: 通过一个起始值,反复利用BinaryOperator来处理和累积元素,返回一个值。
  • count: 返回流中元素的数量。
  • min / max: 找到流中的最小/最大值。
  • anyMatch: 流中是否有一个元素匹配给定的谓词。
  • allMatch: 流中的所有元素是否都匹配给定的谓词。
  • noneMatch: 流中没有任何元素匹配给定的谓词。
  • findFirst: 返回第一个元素。
  • findAny: 返回当前流中的任意元素。

项目中具体用到哪些设计模式

单例模式:确保一个类只有一个实例,例如配置文件管理器。[Spring原本设计好的]
**工厂模式**:创建对象时无需指定具体的类,例如日志工厂。
观察者模式:当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新,例如事件监听。
**策略模式**:定义一系列算法,将每个算法封装起来,并使它们可以互换,例如支付策略。
模板方法模式:在项目中,我有一些具有相同操作步骤但具体实现不同的算法,我使用了模板方法模式来定义这些步骤的骨架,将具体的步骤实现留给子类。任链模式的目的是将请求的发送者和接收者解耦,从而使得多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
责任链模式:它允许将请求沿着处理者链进行发送。收到请求后,每个处理者都有机会对请求进行处理,或者将其传递给链上的下一个处理者。这样,请求就能在一系列处理者中传递,直到有一个处理者对其进行处理为止。 1.递归方式 :在递归模式中,每个处理者内部调用下一个处理者的处理方法。如果当前处理者无法处理请求,它会直接调用下一个处理者的处理方法。这种方式通常是通过递归调用来实现的
2.迭代模式:在迭代模式中,处理者链被构建为一个线性结构,每个处理者都有一个指向下一个处理者的引用。请求从链的第一个处理者开始,依次传递给下一个处理者,直到找到能够处理该请求的处理者为止。这种方式通常是通过循环迭代来实现的
代理模式:为了控制对远程服务的访问,我使用了代理模式。代理负责处理所有与服务对象的交互,并在必要时进行延迟加载。

在我的项目中结合工厂模式策略模式来设计登录接口时,我们可以将登录验证的逻辑抽象为一个策略接口,并为每种登录方式(如:用户名密码登录、手机验证码登录、社交账号登录等)实现具体的策略类。工厂类则负责创建并管理这些策略对象

思考一个问题:哪些方式创建单例模式?

MySQL支持四种隔离级别

第一个是读未提交(readuncomm itted)它解决不了刚才提出的所有问题,一般项目中也不用这个。存在脏读问题 可解决不可重复读 幻读
第二个是读已提交(readcomm ited)它能解决脏读的问题的,但是解决不了不可重复读和幻读。
第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读[解决了一部分],这个也是mysql默认的隔离级别。
第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。
串行化里的读也要加锁 表锁:整个表上锁 行锁:只对一行加锁

什么时候上行锁/表锁?

insert不带查询筛选条件 上行锁底层是索引,b+树底层叶子
update看where后面的条件 带索引加行锁构建b+树 不带索引的加表锁
表锁的速度比行锁速度快

MVCC底层是多版本并发控制 但底层并不怎么了解

深拷贝和浅拷贝的区别?

浅拷贝:只复制对象的基本数据类型和引用类型地址,不复制引用类型指向的对象。如果原对象和浅拷贝对象中的一个改变了引用类型,另一个也会受到影响。旧对象改变新对象也会改变。
深拷贝:复制对象的所有字段,包括基本数据类型和引用类型指向的对象。原对象和深拷贝对象之间不会相互影响。旧对象改变新对象不会改变
Java是值传递

如何实现深拷贝?数组不需要重写【体现了原型设计模式
  • 实现Cloneable接口并重写clone方法 会调用构造方法

    这是最常见的实现深拷贝的方法。首先,你的类需要实现Cloneable接口,然后重写clone()方法构造新对象的过程,并在该方法中调用super.clone(),同时递归地克隆所有引用类型的字段。[如果里面有多层嵌套复杂对象 在每层都要实现Cloneable接口一直重写到基本数据类型的时候才停止]

public class Person implements Cloneable {
    private int age;
    private Address address;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        // 假设Address也实现了Cloneable接口
        cloned.address = (Address) this.address.clone(); 
        return cloned;
    }
}

public class Address implements Cloneable {
    private String street;
    private String city;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  • 通过序列化对象转二进制反序列化二进制转对象。这种方式不需要实现Cloneable接口,但你的类需要实现Serializable接口。反序列化不会调用构造方法

开启线程的时候为什么用的是thread.start方法:

thread.start()方法用于启动一个新线程,并执行该线程的run()方法。调用start()方法后,线程会被放入线程调度队列,等待CPU调度执行。

直接调用run()方法,并不会启动一个新线程,而是在当前线程中执行run()方法,这不符合多线程编程的目的。使用start()方法可以确保线程并发执行,提高程序的性能和响应速度。

java没权限开启一个线程 要调用底层的操作系统 在JVM的底层实现中,会有相应的本地(C或C++)方法来处理线程的创建和管理

你在你们项目中使用过多线程吗?

是的,在我们的项目中,我确实使用过多线程。 【结合项目去说】
在处理大量数据计算或执行耗时的IO操作时,我会使用Java的线程池(如ExecutorService)来并行处理任务,以提高系统的响应速度和吞吐量
image-20250107153246645

sleep和wait的区别

sleep是Thread类的一个静态方法,它使当前线程暂停执行指定的时间,但不会释放锁资源。
wait是Object类的一个方法,它使当前线程暂停执行并释放当前对象上的锁,直到另一个线程调用同一个对象的notify()notifyAll()方法,或者过了指定的等待时间。

sleep是线程内的静态方法 需要指定睡眠的时间 或者自动自己唤醒 不会释放锁
wait是Object类的一个方法 可以指定睡眠时间 不指定就等于无限期 要释放锁
wait一定要搭配synchronized,且都为同一个对象 synchronized锁住了wait万物对象皆为锁
可以被唤醒notify()notifyAll()方法 区别:notify是唤醒一个 notifyall会唤醒全部

普通方法上 锁的是this
静态方法上 锁的是当前类的class对象

ConcurrentHashMap 和 HashTable的区别

是否支持传入NULL

HashMap可以支持为null
若尝试将 null 作为键或值放入 ConcurrentHashMap 将会抛出 NullPointerException
ConcurrentHashMap 不能支持存null

底层实现

ConcurrentHashMap 1.8之前是分段锁来实现 默认是16个HashTable
1.8之后无限接近单个的HashMap 底层用CAS+synchronized
HashTable通过加synchronized锁来控制线程安全

ConcurrentHashMap 读不要加锁 [读写的读也不会加锁] 会走最终一致性
HashTable 读要加锁 [读读都加锁]

为什么要用Redis

高性能:Redis是基于内存的数据结构存储,可以提供高速的数据读写操作。
数据结构丰富:Redis支持多种数据结构,如字符串、列表、集合、散列表、有序集合等,非常适合各种场景。
持久化:Redis支持数据持久化,可以将内存中的数据保存到磁盘中,防止数据丢失。
分布式:Redis支持主从复制、哨兵和集群模式,可以轻松实现分布式缓存。

Redis中缓存了哪些数据

若放登录的信息到Redis的时候 不再用JWT了
Session在集群里面不能用了
替代方案:用Redis 不用JWT
JWT是无状态 无需集中存储

在我们的项目中,Redis中缓存了以下类型的数据:
会话信息:如用户登录信息、购物车内容等。
热点数据:如热门商品信息、推荐内容等。
计数器:如用户访问次数、点赞数、评论数等。
缓存数据库查询结果:减少数据库访问次数,提高系统响应速度。

检测数据存在Redis中,有过期时间吗? 过期时间是多少?仅参考

是的,我们在Redis中缓存的数据通常会设置过期时间,以避免过时的数据占用内存。具体的过期时间取决于数据的类型和业务需求。对于会话信息,我们可能会设置较短的过期时间,如30分钟或1小时;而对于热点数据,可能会设置较长的过期时间,如几小时或一天。具体的过期时间需要根据实际业务场景和数据访问模式来决定。

微服务之间如何调用?

通过注册中心去协调的
首先是有三个重要的概念,服务消费者,注册中心,服务提供者提供者在第一次会把自己的信息注册到注册中心中,比如ip端口,服务功能等消费者需要到注册中心来寻找服务进行消费,在服务消费者第一次请求的时候会拉取服务提供者的信息,注册中心会把提供者的实例列表给到消费者供消费者选择,使用负载均衡来选择服务,默认为轮询,还有加权轮询,随机。同时服务消费者还会定时去注册中心拉取服务提供者的信息如果我们的服务挂掉了怎么办?
服务提供者会每隔一段时间去向注册中心报告自己的状态,如果没有向注册中心报告状态,那么这个时候注册中心会认为服务提供者已经宕机,同时会推送到我们的服务消费者,这个服务提供者已经宕机

微服务的五大组件

  1. 服务注册与发现:如Eureka已过时Nacos、Consul,用于服务的注册和发现。
  2. 配置管理:如Spring Cloud Config、OpenFeign 用于集中管理服务的配置。
  3. 服务网关:如Zuul、Spring Cloud Gateway,作为系统的唯一入口,处理外部请求的路由和过滤。
  4. 负载均衡:如Ribbon,用于在多个服务实例之间分配请求。
  5. 断路器:如Hystrix,用于服务熔断,防止系统雪崩

对于服务注册这块有什么了解?

  • 服务注册中心:服务实例在启动时向服务注册中心注册自己的地址和端口信息。检查 心跳 如果未查询就剔除,同时也有注册中心主动发起请求。
  • 健康检查:服务注册中心通常会定期对已注册的服务进行健康检查,以确保服务的可用性。
  • 服务发现:服务消费者通过服务注册中心查找可用的服务实例,以进行服务调用。
  • 服务去注册:当服务实例关闭或出现故障时,它需要从服务注册中心注销,以避免调用不可用的服务。

你能说一下小程序的登录流程吗?

调用微信api,根据code获取openid;根据openid查询用户为空就新增;调用微信api WechatService + WechatServiceImpl(openId+phoneCode) 获取用户绑定的手机号;保存或修改该用户;将用户id存入token返回(JWT生成token)

有哪些方式可以创建单例?

  1. 饿汉式:在类加载时就立即初始化并创建单例对象。
  2. 懒汉式:在第一次调用时初始化单例对象,通常需要考虑线程安全问题。
  3. 双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。
  4. 静态内部类:利用静态内部类的加载机制来确保单例对象的唯一性。
  5. 枚举:利用枚举的特性,保证单例对象的唯一性和线程安全【不可用反射】
并发情况下严格控制单例?volatile→禁止进行指令重排序

双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。

公平锁和非公平锁的区别?

  • 公平锁:多个线程按照它们请求锁的顺序来获取锁,先来先得。这种方式不会产生饥饿现象,但可能会降低吞吐量,因为需要额外的开销来维护等待队列的顺序。【僵尸线程】对资源顺序有严格要求
  • 非公平锁:线程获取锁的顺序不一定是按照请求锁的顺序,允许线程“插队”。这种方式可能会提高吞吐量,但可能导致某些线程长时间无法获取锁,产生饥饿现象。【为什么性能高?不用沉睡和阻塞 避免来回切换】对性能要求高

ReentrantLock 看传参 公平/非公平都支持
Synchronized 公平锁

SpringMVC的工作原理

  • 前端的HTTP请求到达时首先被DispatcherServlet接收

  • DispatcherServlet根据请求信息路径查找合适的HandlerMapping来确定哪个Controller应该处理该请求。

  • 找到合适的Controller后,DispatcherServlet将请求转发给它处理。

  • Controller处理完请求后返回一个ModelAndView对象给DispatcherServlet。

  • DispatcherServlet再通过ViewResolver解析ModelAndView中的视图逻辑名,找到对应的视图。

  • 最后,DispatcherServlet将模型数据渲染到视图上并响应给客户端。

OpenFeign的底层原理

OpenFeign 实现了简洁、声明式的 HTTP 请求调用,并且与 Spring Cloud 集成后能提供更多的功能如负载均衡等

动态代理: OpenFeign 使用 Java 动态代理技术,基于接口创建代理类,代理类会自动发起 HTTP 请求。你定义的接口方法会映射到 HTTP 请求上,OpenFeign 会根据注解(如 @RequestMapping, @GetMapping 等)来构建请求。

注解解析: OpenFeign 会解析接口方法上的注解,构造 HTTP 请求的 URL、请求方法类型(GET、POST 等),以及请求体和请求头等信息。

请求拦截和处理: 在请求发起之前,OpenFeign 允许通过拦截器(RequestInterceptor)来修改请求,比如设置请求头、参数等。

负载均衡与容错: 如果与 Spring Cloud 一起使用,OpenFeign 会集成 Ribbon(负载均衡)和 Hystrix(容错),使得服务调用更加健壮和可靠。

序列化与反序列化: OpenFeign 会利用 Jackson 等库进行请求和响应的序列化和反序列化,将 Java 对象与 HTTP 请求/响应内容相互转换

在使用OpenFeign时,开发者只需要定义接口并添加相应的注解,OpenFeign会在运行时动态生成实现类来执行HTTP请求。

对Volatile的理解

确保了不同线程对这个变量进行读写操作时的可见性。
是java的关键字是修饰共享的变量,不能修饰局部变量。
修饰普通或静态成员变量,主要用来保证可见性和有序性。

Spring Security的实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息

什么是AQS

是多线程中的抽象队列同步器。是一种锁机制,它是做为一个基础框架使用的,是一个抽象类。
像ReentrantLock都是基于AQS实现的
定义了一个并发情况下一些抽象的资源 资源能否共享/独享 定义了公平和非公平
如果是非公平锁如果来了个新的线程来抢线程 也是会去抢一次
AQS成为了JUC很多类都去继承的 它抽象了很多并发的属性和行为,让子类去继承它扩展自己

Synchronized的锁升级

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低
  • 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题

一段很长的时间内都只被一个线程使用锁 偏向锁
有线程交替或线程加锁的时间是错开的 轻量级锁
有很多线程来抢 重量级锁

java语言是高级语言如果想调用底层的操作系统和硬件要通过操作系统的API去操作。以前老的JDK版本 数据是在操作系统找的数据,Monitor的标志0 和 1,底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 。
引入新型锁后,java里是用对象头找个地方存一把锁,这样就不涉及到调用操作系统底层。一开始new了个对象 此时是无锁状态。接下来来人拿锁,长期一个人拿到那个锁 此时是偏向锁竞争不激烈。后面多线程一起来 交替抢锁 此时是轻量级锁。随着并发越来越高 此时在一个线程拿到锁后很多线程来抢锁 线程先尝试自己先获取几次(自旋锁64次未拿到锁就会升级为重量级锁) 这时就涉及到操作系统的底层对象涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低。锁不可逆可能新版本可以降级

Dockerfile 常用命令

  • FROM: 指定基础镜像。
  • ENV: 设置环境变量。
  • RUN: 执行命令并创建新的镜像层。
  • COPY: 将文件从宿主机复制到容器中。
  • EXPOSE: 声明容器运行时将监听的端口。
  • ENTRYPOINT: 配置容器启动时运行的命令。

常用的 Docker 命令

  • docker run: 创建一个新的容器并运行一个命令。
  • docker pull: 从仓库中拉取或者更新一个镜像。
  • docker push:推送镜像到服务
  • docker build: 从 Dockerfile 构建一个镜像。
  • docker images: 列出本地镜像。
  • docker ps: 列出运行中的容器。
  • docker stop: 停止一个运行中的容器。
  • docker start: 启动一个停止的容器。
  • docker rm: 删除一个容器。
  • docker rmi: 删除一个镜像。
  • docker exec: 在运行中的容器内执行命令。
  • docker logs: 获取容器的日志。
- docker volume create:创建数据卷
- docker volume ls:查看所有数据卷
- docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置
- docker volume rm:删除指定数据卷

Docker Compose 常用命令

  • docker-compose up: 启动所有服务的容器。
  • docker-compose down: 停止并删除容器、网络、卷和镜像。
  • docker-compose ps: 列出项目中所有的容器。
  • docker-compose exec: 进入指定的容器。
  • docker-compose build: 构建或重建服务。
  • docker-compose logs: 查看服务的日志输出。
  • docker-compose stop: 停止运行的容器。

synchronized 和 ReentrantLock 的区别

  • synchronized 是Java的一个关键字用于方法和代码块中,而 ReentrantLock 是JUC包的一个类。
  • synchronized 可以自动加锁和解锁,而 ReentrantLock 需要手动加锁和解锁。
  • synchronized 的锁是非公平的,而 ReentrantLock 默认也是非公平的,但可以设置为公平锁。

你们公司是怎么部署项目的

是通过docker + jenkins
测试环境我们参与 生产环境组长部署

varchar 与 char 区别

  • varchar 是可变长度的字符串,而 char 是固定长度的字符串。
  • varchar 的性能通常比 char 差,因为需要处理额外的长度信息。
  • 当数据长度变化很大时,推荐使用 varchar;当数据长度几乎固定时,使用 char 可能更合适。

Redis的持久化有哪几种? 它们的区别是什么?

完整性 大小 数据恢复速度 建议

Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。

[root@localhost ~]# redis-cli
127.0.0.1:6379> save          #由Redis主进程来执行RDB,会阻塞所有命令
ok

127.0.0.1:6379> bgsave        #开启子进程执行RDB,避免主进程受到影响
Background saving started

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

==RDB的执行原理?==数据完整性高用RDB

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

优点:二进制数据重启后 Redis无需过多解析 直接恢复

==AOF==对数据不敏感要求不高

AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义通过执行bgrewriteaof命令,可以让AOF文件执行重读功能,用最少的命令达到相同效

Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置

# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

★★★★★★★★ RDB与AOF对比 ★★★★★★★★

RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)

RDB AOF
持久化方式 定时对整个内存做快照哦 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源
但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

项目中是怎么使用redis的

需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题

用Hash类型 大Key是Id 小key是商品id value是商品数量
数据量点击量 用String类型
用Set类型 Zset做排行榜

你的项目中哪里使用到分布式锁?

==Redis分布式锁==

Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写

  • 获取锁

    添加锁,NX是互斥、EX是设置超时时间
    SET lock value NX EX 10

  • 释放锁

    释放锁,删除即可
    DEL key

你可以说一下redis的分布式锁的原理吗

我在项目中是集成了redisson(底层基于Lua脚本[具有原子性])

==redisson实现分布式锁 - 执行流程==

加锁 ↓→ 加锁成功 → Watch dog(看门狗)每隔(releaseTime/3的时间做一次续期) → Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis

加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓

public void redisLock() throws InterruptedException{
    RLock lock = redissonClient.getLock("heimalock");
 // boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    if(isLock){
        try{
            sout("执行业务");
        } finally{
            lock.unlock();
        }
    }
}

==redisson实现分布式锁 - 可重入==

redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id重入次数

KEY VALUE VALUE
field value
heimalock thread1 0
public void add1(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
  add2();
// 释放锁
  lock.unlock();
}
public void add2(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
  lock.unlock();
}

==redisson实现分布式锁 - 主从一致性==

Redis Master主节点:主要负责写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读

当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?

  • RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想zookeeper

redis和mysql怎么保证数据一致性

先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行
你如果只写redis 万一丢了数据怎么办
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少

一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)

① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据

② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

  • 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

  • 写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]

  • ==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】有过期时间兜底

    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
    • 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)排他锁确保了写操作的原子性和一致性
    • 读数据的时候添加共享锁(读不互斥、写互斥)
    • 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)

    redissionClient.getReadWriteLock(“xxxx”);

  • ==异步通知:==异步通知保证数据的最终一致性(需要保证MQ的可靠性)需要在Redis中更新数据的同时,通知另一个服务进行某些操作。

    • 使用场景
      • 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
      • 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
      • 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
  • ==基于Canal的异步通知==:监听mysql的binlog
    可以解析binlog文件 可以存放mysql里面的数据 看最近有无增删改查 转换成redis命令 再给redis里面

    • 使用MQ中间件,更新数据之后,通知缓存删除
    • 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存

synchronized可以作用在哪些地方,分别锁的是什么

在Java中,synchronized关键字可以用来实现线程同步,它可以作用在不同的地方,并且锁定的对象也不同:

  1. 实例方法

    • 作用在实例方法上时,锁的是调用该方法的对象实例(即**this对象**)。
    • 任何线程想要执行这个方法,都必须获得该对象实例的锁。
    public synchronized void synchronizedMethod() {
        // 方法体
    }
    
  2. 静态方法

    • 作用在静态方法上时,锁的是类的Class对象
    • 由于静态方法是属于类的,而不是属于任何特定实例,所以所有线程要想执行这个静态同步方法,都必须获得该类的Class对象的锁。
    public static synchronized void synchronizedStaticMethod() {
        // 方法体
    }
    
  3. 代码块

    • 作用在代码块上时,可以指定一个锁对象括号里的对象,可以是任何对象
    • 当进入这个代码块时,线程必须获得指定锁对象的锁。
    public void synchronizedBlock() {
        synchronized(this) { // 锁定当前对象实例
            // 代码块
        }
    }
    
    public void synchronizedBlockWithObject() {
        Object lock = new Object();
        synchronized(lock) { // 锁定指定的对象
            // 代码块
        }
    }
    

什么情况下索引会失效?

  • 违反最左前缀法则
  • 范围查询右边的列,不能使用索引
  • 不要在索引列上进行运算操作,索引将失效
  • 字符串不加单引号,造成索引失效。(类型转换)
  • 以%开头的Like模糊查询,索引失效
    [不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]

索引创建原则有哪些?索引很多就会有很多B+树

数据量较大,且查询比较频繁的表
常作为查询条件、排序、分组的字段 [where、group by、order by]
③ 字段内容区分度高
④ 内容较长,使用前缀索引
尽量联合索引对存储节省空间

如果我们经常根据客户ID和订单日期来查询订单,那么可以在 customer_id 和 order_date 上创建一个联合索引。
CREATE INDEX idx_customer_date ON orders (customer_id, order_date);

这个联合索引 idx_customer_date 有以下几个特点:

索引顺序:首先根据 customer_id 排序,然后在每个 customer_id 的基础上根据 order_date 排序。
查询优化:以下查询可以利用这个联合索引:
SELECT * FROM orders WHERE customer_id = ? AND order_date = ?;
SELECT * FROM orders WHERE customer_id = ?;

要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
大字段不建议建立索引是因为B+树一个叶子节点/一个非叶子节点 差不多16k 一个节点对应一个[页] 多的话会更多层
尽量不用性别去创建索引

  • 先陈述自己再实际工作中是怎么用的
  • 主键索引
  • 唯一索引
  • 根据业务创建的索引(复合索引)

索引的底层数据结构了解过吗?

MySQL的InnoDB引擎采用的B+树的数据结构来存储索引

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树

==B Tree(矮胖树)==,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构

B树与B+树对比

  • 磁盘读写代价B+树更低
  • 查询效率B+树更加稳定
  • B+树便于扫库和区间查询

B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据

B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定

便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]

mysql底层为什么用B+树利用二分查找,树越矮经过磁盘IO次数越少,它是稳定的每次都查到最底层

二叉树 O(logn) 容易退化成链表 所以不用它
平衡二叉树 全部倾斜
红黑树 一个节点只能存一个数据
B树能不能除了叶子节点其他不存数据呢?

你可以设计一种变体的B树,其中只有叶子节点存储数据,而所有其他非叶子节点仅作为导航节点,不存储实际的数据。这种结构在概念上类似于B树的一个特例,通常被称为B树索引结构的一部分,其中非叶子节点存储的是键值,而叶子节点存储的是实际的数据记录或者是指向数据记录的指针

B+树第三层2000多万条数据,尽量不要把数据达到2000多万
B+树叶子节点加了双向链表 让我们查询更加稳定 范围查询会更快

mysql索引底层不一定只有B+树,也可能是Hash 在精准查询性能比它高

R—Tree:地理位置搜索

联合索引

where b= AND c= AND a= 这样走索引都能走 底层自己排序
为什么联合索引要遵循最左匹配原则【里面的b+树 先按照a排序 再b 因为要二分查找 不排序怎么找?】

在MySQL中,如何定位慢查询?查询前用explain查询是否走了索引等问题

explain查询后的列:
id:查询中SELECT语句的序列号,如果该行引用其他行的并集结果,则该值可以为空。
select_type:表示查询的类型,常见的类型有:
SIMPLE:简单的SELECT查询,不使用UNION或子查询。
PRIMARY:最外层的SELECT查询。
UNION:在UNION中的第二个或随后的SELECT查询。
DEPENDENT UNION:在UNION中的第二个或随后的SELECT查询,取决于外层查询。
UNION RESULT:UNION的结果。
SUBQUERY:子查询中的第一个SELECT。
DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外层查询。
table:查询的是哪个表。
type:这是你提到的一个非常重要的列,它表示MySQL在表中找到所需行的方式,也称为“访问类型”。以下是一些常见的访问类型,从最好到最差排序:
system:表只有一行(系统表)。
const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,所以优化器的其余部分可以将这一行视为常量。
eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这通常是最好的联接类型,除了const类型。
ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。
fulltext:使用全文索引执行查询。
ref_or_null:与ref类似,但是MySQL会额外搜索包含NULL值的行。
index_merge:表示查询使用了两个或更多的索引。
unique_subquery:用于IN子查询,子查询返回不重复的值集。
index_subquery:用于IN子查询,子查询返回不重复的值集,可以使用索引。
range:使用索引来检索给定范围的行。
index:全索引扫描(比ALL快,因为索引通常比数据行小)。
ALL:全表扫描,这是最差的一种类型,因为MySQL必须检查每一行以找到匹配的行。
possible_keys:指出MySQL能使用哪些索引来优化查询。
key:MySQL实际决定使用的索引。
key_len:使用的索引的长度。越短越好。
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:MySQL认为必须检查的用来返回请求数据的行数。
filtered:显示了通过条件过滤出的行数的百分比估计。
Extra:包含MySQL解析查询的额外信息,例如是否使用了索引,是否排序了结果,是否使用了临时表等

1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)

产生原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]

  • 调试工具Arthas
  • 运维工具:Prometheus、SKywalking(接口访问时间)

方案二:==MySQL自带慢日志==

慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:

# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2

什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?

  • 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个【id存放的b+树】
  • **非聚簇索引(二级索引)**:数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个【叶子就是id的字段】
  • 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表

怎么避免回表 → 使用覆盖索引!
需要name 直接 select name 而不用 select *
要按需来查找

除了InnoDB,MySQL数据库还支持多种其他存储引擎,其中最著名的是MyISAM。以下是InnoDB和MyISAM两个存储引擎的主要区别:

事务支持:
InnoDB:支持事务,它遵循ACID原则(原子性、一致性、隔离性和持久性)。如果事务中的某个操作失败,整个事务可以回滚到开始状态。
MyISAM:不支持事务,这意味着你无法回滚操作,这对于数据完整性和恢复可能是一个问题。
    
锁定机制:
InnoDB:使用行级锁定,只锁定需要的特定行,这可以大大减少数据库操作的冲突。
MyISAM:使用表级锁定,每次操作都会锁定整个表,这在并发操作较多时可能导致性能问题。
    
崩溃恢复:
InnoDB:具有自动崩溃恢复功能,即使数据库崩溃,也不会丢失数据,因为它将事务日志写入磁盘。
MyISAM:在崩溃后恢复较为困难,可能会丢失数据,因为它不记录事务日志。
    
全文搜索:
InnoDB(MySQL 5.6及以后版本):支持全文索引,但功能上不如MyISAM的全文搜索强大。
MyISAM:提供了更强大的全文搜索功能,但在MySQL 5.6之前,这是MyISAM相对于InnoDB的主要优势。
    
存储限制:
InnoDB:表的大小理论上受限于操作系统的文件大小限制,通常可以处理更大的数据量。
MyISAM:表的大小受限于最大文件大小,通常是2GB到4GB,这取决于文件系统的限制。
    
外键支持:
InnoDB:支持外键约束,这有助于保持数据的引用完整性。
MyISAM:不支持外键约束。
    
存储空间:
InnoDB:通常需要更多的存储空间,因为它存储了额外的信息来支持事务和行级锁定。
MyISAM:通常占用更少的存储空间,因为它不需要存储这些额外的信息
分类 含义 特点
==聚集索引(Clustered Index)== 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有, 而且只有一个
==二级索引(Secondary Index)== 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引

==回表查询==

select * from user where name = 'Arm';

MySQL的日志文件有哪些,它们的作用是什么

MySQL的日志文件有哪些,它们的作用是什么?

MySQL主要有以下几种日志文件:

  • 错误日志(Error Log):记录MySQL服务的启动、运行或停止过程中的错误信息。
  • 查询日志(General Query Log):记录所有MySQL执行的SQL命令,无论这些命令是否正确执行。
  • 慢查询日志(Slow Query Log):记录执行时间超过指定阈值的查询语句。
  • 二进制日志(Binary Log)记录所有更改数据的SQL语句,用于主从复制和数据恢复。事务的提交 和 主从复制
  • 事务日志/重做日志(InnoDB Redo Log)记录InnoDB存储引擎的事务操作,用于崩溃恢复。
  • 回滚日志/撤销日志(InnoDB Undo Log)用于事务回滚,保证事务的原子性。

undo log 和 redo log的区别?

redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undo log保证了事务的原子性和一致性

  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

==redo log==

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

==undo log==

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志

  • 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。

undo log可以实现事务的一致性和原子性

MySQL主从同步原理?

MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
从库重做中继日志中的事件,将改变反映它自己的数据。

主服务器(Master)上的数据更改(如INSERT、UPDATE、DELETE操作)会被记录到二进制日志中。
从服务器(Slave)上的I/O线程连接到主服务器,请求主服务器上的二进制日志。
主服务器将二进制日志发送给从服务器,从服务器将这些日志事件写入到本地的中继日志(Relay Log)。
从服务器上的SQL线程读取中继日志中的事件,并在本地执行这些事件,从而实现数据的复制。

MySQL主从复制的核心就是二进制日志

二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

复制分成三步:
  • Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  • slave重做中继日志中的事件,将改变反应他自己的数据

项目中哪里涉及到分布式事务问题? 你是怎么解决的? 能说一下

分布式事务问题可能出现在跨多个服务或数据库的操作中,例如在订单服务中同时更新订单状态和扣减库存。秒杀案例:是先扣库存提前缓存到redis里,判断够不够,用RabbitMQ异步下来

解决方案:可以使用分布式事务框架,如Seata,其中AT模式是一种常见的解决方案。

AT模式原理:

  • AT模式基于两阶段提交,分为两个阶段:一阶段 prepare 和二阶段 commit/rollback。
  • 在业务方法开始时,Seata会拦截业务SQL,记录业务数据在执行前后的镜像,生成行锁。
  • 如果一阶段 prepare 成功,则二阶段进行 commit,直接提交事务;如果 prepare 失败,则执行 rollback,利用之前保存的数据镜像回滚到执行前的状态。

项目中哪里用到MQ,用来干什么?

异步发优惠卷 + 积分 [用户对于实时性要求不是很高]

  • 使用MQ的场景:订单处理
    具体场景

    当用户在电子商务平台上成功下单后,订单服务需要执行以下操作:

    1. 更新订单状态为“已支付”。
    2. 扣减商品库存。
    3. 通知支付服务处理支付。
    4. 通知物流服务准备发货。
    使用MQ的原因

    在这些操作中,更新订单状态和扣减库存是实时且同步的操作,但通知支付服务和物流服务则可以异步进行。使用MQ可以帮助我们实现以下目标:

    • 解耦服务:订单服务不需要直接调用支付服务和物流服务,降低了服务间的耦合度。
    • 异步处理:订单服务可以立即响应客户端,不必等待支付和物流服务的处理结果。
    • 流量削峰:在高峰期,MQ可以缓冲大量的订单处理请求,避免服务被压垮。

订单服务生产消息: 当订单服务完成订单状态更新和库存扣减后,它将以下消息发送到

{
  "orderId": "123456789",
  "status": "paid",
  "userId": "user123",
  "items": [
    {"productId": "prod123", "quantity": 1},
    {"productId": "prod456", "quantity": 2}
  ]
}

这个消息将被发送到不同的主题或队列,例如payment_topiclogistics_topic

2. 支付服务和物流服务消费消息:

  • 支付服务订阅payment_topic,当接收到订单支付消息后,它会处理支付逻辑,如验证支付状态、记录交易日志等。
  • 物流服务订阅logistics_topic,当接收到订单消息后,它会准备发货,更新物流信息,并通知用户。

通过这种方式,订单服务可以快速响应用户请求,而支付和物流服务可以按照自己的节奏处理订单相关的操作,整个系统因此变得更加灵活和可扩展。

如何保证消息不丢失?

保证生产者能够成功发送到交换机和队列(存储消息),生产者提供了消息确认机制
到队列后消息要有持久化机制
消费者要有一个消息确认机制 保证消费者至少消费成功消息一次

  • 开启生产者确认机制,确保生产者的消息能到达队列
    confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去

  • 开启持久化功能,确保消息未消费前在队列中不会丢失
    万一broker挂掉就惨了 保证至少成功一次消费

  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
    消费者三种机制:

    RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息,而SpringAMQP则允许配置三种确认模式:

    • manual:手动ack,需要在业务代码结束后,调用api发送ack。

    • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

  • 异步发送(验证码、短信、邮件)
  • MySQL和Redis,ES之间的数据同步
  • 分布式事务
  • 削峰填谷

如何解决消息积压?

产生原因:当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限

如何保证消费幂等性【MQ】

幂等性是指同一个操作执行多次和执行一次的效果相同。在消息消费的场景中,保证幂等性通常有以下几种方法:
利用数据库的唯一约束
在数据库中为消息设置唯一标识(如消息ID),在处理消息前先检查该标识是否已存在。
导致重复消费 返回ack,blocker未收到。一定要在
生产者
投递的时候生成全局唯一的id,消费者就会去判断。异步生成 拿订单号去数据库查 如果查得到就直接return
精髓就是全局唯一
UUID不行 因为每次发送的消息都不是同一个UUID 要用业务上的

哪些地方还会有幂等问题?

提供者的openfegin、xxl-job、被别人调用且涉及到增删改

状态记录:

在消费消息前,记录消息的状态(如已处理),处理完毕后再更新状态。要根据订单ID+状态 来保证消费幂等性。订单存在且未支付 → 更新锁订单ID

并发情况幂等性:

完美的幂等要加上分布式锁对敏感性要求高,且要控制好锁的力度

如何保证消费有序性

队列中可以指定消息的消费顺序

RabbitMQ → 多个生产者并发投,所以生产者不能保证有序性,只考虑消费有序性。以消息进入的MQ的消息去回答。

怎么保证?
单线程消费:

在消费者端使用单个线程处理消息,确保消息按顺序处理。

分区有序:

在如Kafka这样的消息队列中,可以保证同一个分区内的消息是有序的。

如何既要又要【有序 + 速度快】

既要又要”通常指的是在保证消息的幂等性和有序性的同时,还需要考虑其他特性(如高性能、高可用等)
在一些场景下,可能需要在性能和一致性之间做权衡。例如,可以选择最终一致性来换取更高的性能。
Kafka 和 RocketMQ可以天生实现【底层Hash取模】

若非要用RabbitMQ实现呢?

不同订单之间是否要求一定顺序??
镜像集群,先搭3个节点的镜像集群,建立三个队列分为不同的镜像节点 各占一个队列,需要自己去实现
对订单号进行hash取模看到落到哪个节点
三个队列至少三个消费者 分别去消费它们
此时就可以并行有三个消费者去执行
把业务数据没关系的放在不同的队列去管理

万一挂掉了呢?

队列有持久化不用担心

能说一下如何使用死信交换机吗支付

死信交换机(DLX)用于处理无法正常消费的消息
创建一个正常的交换机和队列,以及一个死信交换机和死信队列。

  1. 定义死信交换机:创建一个用于处理死信的交换机。
  2. 定义死信队列:创建一个队列用于接收死信,并将其绑定到死信交换机。
  3. 配置主队列的死信交换机属性:在主队列上设置参数,指定当消息无法正常处理时应该发送到哪个死信交换机。
  4. 发送消息到主队列:生产者将消息发送到主交换机,进而路由到主队列。
  5. 消费主队列消息:消费者从主队列中获取消息并进行处理。如果消息处理失败,它将被路由到死信交换机。
  6. 消费死信队列消息:设置消费者来处理死信队列中的消息,进行错误处理或记录日志等操作。

mysql如何提升深分页查询效率子查询+索引

使用索引

  • 确保查询中使用的列上有适当的索引,这样可以加快查找速度。

**避免使用OFFSETLIMIT**:

  • 使用OFFSET进行深分页时,MySQL需要遍历所有OFFSET之前的行。可以通过记住上一次查询的最大ID来避免使用OFFSET

使用条件过滤

  • 如果可能,使用WHERE子句来减少需要扫描的数据量。

增加LIMIT的大小

  • 如果业务允许,可以增加每次查询返回的结果集大小,减少分页次数。

缓存

  • 对于不经常变更的数据,可以使用缓存来存储已经查询过的页。

使用EXPLAIN分析查询

  • 使用EXPLAIN来分析查询计划,找出性能瓶颈并进行优化。

能说一下常用的存储引擎以及它们的差异吗

InnoDB
支持事务、行级锁和外键。
适合处理大量短期事务。
为了维护数据的完整性,写操作相对较慢

MyISAM

不支持事务、不支持行锁只支持表锁
并发没那么大 事务要求没那么高可以用

能说一下倒排索引的原理吗?

根据参与文档中的字段 要构建倒排就会去分词
根据用户索引也会分词 就会去查文档id 再去查文档
中文词库为IK (Ikun 你干嘛 哎哟~)

es的text和keyword的区别

text

用于全文搜索,会分词,字符串类型

keywod

用于精确搜索字段,不会被分词,字符串类型

es在你的项目中是用来做什么的

快速搜索商品(C端)、订单(后台) + 日志查询 + 地理位置搜索经纬度定位附近的事物

mysql和ElasticSearch如何做数据同步

mysql进行增删改的时候
对数据敏感性实时性要求没那么高 只看可靠性[MQ异步 + 定时任务 = 没有那么强一致性]

如果数据量没那么大 有没有必要上ES?

没有必要,正排索引不走全表扫描也蛮快
组长进行技术选型 考虑到以后的业务增长

项目已经上线了 但是中途想换成ES 怎么办

mysql是全量数据 mq只能同步增量数据 怎么办呢?
新上架的只能到ES 那应该如何?

此时涉及到全量和增量的同步与Redis不一样
加定时任务每周/每天 会定期重构一次索引库晚上跑→兜底模式,全量同步,后期再增量同步

能说一下分词的原理吗

底层是大数据量的内容 树的结构来构建分词 IK,字符分割、词汇识别、过滤停用词
不好意思面试官 具体底层原理不是很了解

使用ES有遇到什么问题吗

类似于深分页!
测试环境数据量不会很大 等到上线后才会有这种问题

阅读全文

SpringBoot入门及电商项目[SpringBoot_MyBatis.generator_Swagger2_Redis_Postman_RESTful_AOP_UUID_QRCode_Linux_Aliyun]

2024/2/28

Spring Boot电商项目入门与实战

  • 重量级的电商项目,前后端彻底分离
  • 体验企业级开发流程:需求分析、设计、编码、测试、上线
  • 用户管理、商品分析、商品管理、购物车、订单等核心功能
  • 数据库的设计与实现
  • 应用RESTful进行接口设计,并使用Postman进行接口测试
  • 应用MyBatis generator代码自动生成插件,提升开发效率
  • 使用阿里云完成项目的线上部署
  • 使用Swagger2构建强大的API文档
  • 使用Redis对商品分类信息进行缓存
  • 使用JSR-303实现请求参数校验

Spring Boot入门

软件版本
  • Java 1.8.0
  • MySQL 8.0.12
  • Maven 3.3.9
  • Spring Boot 2.2.1(严格一致)

Spring Boot诞生历史

  • Spring的缺点:配置过于繁琐
  • Spring Boot开发团队:Pivotal

Spring Boot简介

  • 简化初始搭建以及开发过程
  • 不再需要定义样板化的配置
  • 快速应用开发领域

Spring、Spring MVC、Spring Boot

  • Spring最初利用IOC和AOP解耦
  • 按照这种模式搞了MVC框架
  • 写很多样板代码很麻烦,就有了Spring Boot
  • Spring Cloud是在Spring Boot基础上

Spring Boot核心特点

  • 开箱即用
  • 约定优于配置

Spring Boot版本介绍

  • CURRENT 2.2.1 CURRENT GA [最新版本]
  • GA 2.2.2 SNAPSHOT [GA=General Availability 面向大众稳定版本 版本永恒不变]
  • SNAPSHOT 2.1.11 SNAPSHOT [快照 版本随时被修改]
  • 如何选择版本? 2.1.10 GA

新建Spring Boot项目演示

  • IDEA集成的Spring Initializr

第一个接口开发

com/imooc/springbootlearn/SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootlearnApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }

}
com/imooc/springbootlearn/ParaController.java
package com.imooc.springbootlearn;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 演示各种传参形式
 */
@RestController
public class ParaController {
    @GetMapping({"/firstrequest"})
    public String firstRequest(){
        return "第一个Spring Boot接口";
    }
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>SpringBootlearn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBootlearn</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

多种配置URL的方式

com/imooc/springbootlearn/ParaController.java
package com.imooc.springbootlearn;

import org.springframework.web.bind.annotation.*;

/**
 * 演示各种传参形式
 */
@RestController
@RequestMapping("/prefix")//统一增加公共前缀
public class ParaController {
    @GetMapping({"/firstrequest"})
    public String firstRequest() {
        return "第一个Spring Boot接口";
    }

    //@RequestParam 从请求中找到这个参数进行绑定
    @GetMapping({"/requestpara"})
    public String requestpara(@RequestParam Integer num) {
        return "para from request: " + num;
    }

    //@PathVariable 从URL中寻找对应的参数进行绑定
    @GetMapping({"/para/{num}"})
    public String pathpara(@PathVariable Integer num) {
        return "para from path: " + num;
    }

    //多URL的用法
    @GetMapping({"/multiurl1", "/multiurl2"})
    public String multiurl(@RequestParam Integer num) {
        return "para from path: " + num;
    }

    //增加程序健壮性 不一定必须传值
    @GetMapping({"/required"})
    public String required(@RequestParam(required =
            false, defaultValue = "0") Integer num) {
        return "para form request: " + num;
    }
}

Web项目的三层结构

  • Controller职责:对外暴露接口
  • Service职责:复杂业务场景下对业务逻辑做一层抽象和封装,保持Controller的简洁和独立,抽象出来的Service可以被Controller重复调用。具体业务代码写在service层,Controller只做简单的逻辑判断
  • DAO层职责:和数据相关的、增删改查数据库代码

配置文件简介

  • properties:容器端口名、数据库信息、日志级别…

  • yml:分层级,冒号后需要空格

可以自动转换的网址:在线yaml转properties-在线properties转yaml-ToYaml.com

Properties:
server.port=8081
server.servlet.context-path=/first
============================================================
YAML:
server:
  port: 8081
  servlet:
    context-path: /first

进行自定义配置

注解类配置载入数据[利用@Value注解]

com/imooc/springbootlearn/PropertiesController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 演示读取配置的Controller
 */
@RestController
public class PropertiesController {
    //用@Value将properties的设置值进行绑定
    @Value("${school.grade}")
    Integer grade;
    @Value("${school.classnum}")
    Integer classnum;
    @GetMapping("/gradeclass")
    public String gradeClass(){
        return "年级: " + grade + " 班级:" + classnum;
    }
}
resoources/application.properties
#server.port=8081
##对于整个项目建立统一的前缀 http://127.0.0.1:8081/first/prefix/required
#server.servlet.context-path=/first
#school.方便分类
school.grade=3
school.classnum=6

配置类文件配置载入数据

com/imooc/springbootlearn/SchoolConfig.java
package com.imooc.springbootlearn;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * School配置类
 */
@Component
@ConfigurationProperties(prefix = "school")
public class SchoolConfig {
    //自动去配置信息里寻找且绑定
    Integer grade;
    Integer classnum;

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }

    public Integer getClassnum() {
        return classnum;
    }

    public void setClassnum(Integer classnum) {
        this.classnum = classnum;
    }
}
com/imooc/springbootlearn/ConfigController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 读取配置类
 */
@RestController
public class ConfigController {
    @Autowired
    SchoolConfig schoolConfig;
    @GetMapping({"/gradefromconfig"})
    public String gradeclass(){
        return "年级: " + schoolConfig.grade + " 班级:" + schoolConfig.classnum;

    }
}

Service和Dao的编写

  • 学生信息查询案例
com/imooc/springbootlearn/Student.java
package com.imooc.springbootlearn;

/**
 * 学生实体类
 */
public class Student {
    Integer id;
    String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
com/imooc/springbootlearn/StudentController.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 学生Controller
 */
@RestController
public class StudentController {
    @Autowired
    StudentService studentService;
    //服务层 去中转

    @GetMapping("/student")
    public String student(@RequestParam Integer num){
        Student student = studentService.findStudent(num);
        return student.toString();
    }
}
/*
联系
@Autowired和@Resource注解都是作为bean对象注入的时候使用的
两者都可以声明在字段和setter方法上
注意:如果声明在字段上,那么就不需要再写setter方法。但是本质上,该对象还是作为set方法的实参,通过执行set方法注入,只是省略了setter方法罢了

区别
@Autowired注解是Spring提供的,而@Resource注解是J2EE本身提供的
@Autowird注解默认通过byType方式注入,而@Resource注解默认通过byName方式注入
@Autowired注解注入的对象需要在IOC容器中存在,否则需要加上属性required=false,表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错
*/
com/imooc/springbootlearn/StudentService.java
package com.imooc.springbootlearn;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 学生Service  之后调用mapper操作数据库[interface]
 */
@Service
public class StudentService {
    @Autowired
    StudentMapper studentMapper;

    public Student findStudent(Integer id){
        return studentMapper.findById(id);
    }
}
com/imooc/springbootlearn/StudentMapper.java 【接口】
package com.imooc.springbootlearn;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 学生Mapper
 */
@Mapper
@Repository
public interface StudentMapper {
    @Select("select * from students where id = #{id}")
    Student findById(Integer id);
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>springbootlearn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBootlearn</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
com/imooc/springbootlearn/SpringBootlearnApplication.java
package com.imooc.springbootlearn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages="com.imooc")
public class SpringBootlearnApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootlearnApplication.class, args);
    }

}
application.properties
#server.port=8081
#server.servlet.context-path=/first
school.grade=10
school.classnum=6

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root


SpringBoot电商项目

慕慕生鲜前台
慕慕生鲜后台

课程整体介绍

  • 为什么要做电商项目
  • 项目亮点以及功能模块介绍
  • 项目演示
  • 项目开发所需要工具准备
  • 数据库设计与项目初始化
  • 功能模块开发
  • 阿里云部署
  • 项目总结

项目亮点

  • 最新的业界的互联网企业的优质技术
  • 代码规范简介,充分优化
  • 流程完整、电商功能丰富
  • 前后端彻底分离,符合未来趋势,学了就能用

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

项目演示

  • 前后端分离
  • 接口文档
  • 全栈、CTO
  • 核心是接口的设计

项目开发所需工具准备

  • IDEA常用优质插件介绍
    • Maven Helper
    • Free MyBatis plugin [跳转/识别mapper语法错误]
  • Postman安装和常用功能演示
  • MySQL可视化工具

数据库设计与项目初始化

  • 表设计
  • 技术选型、思路
  • 新建项目,整合Mybatis,跑通接口
  • 引用log4j2日志组件
  • 使用AOP统一处理Web请求日志 [请求的参数 返回的商品、字段]

技术选型

  • Spring Boot 2.2.1RELEASE
  • MyBatis 3.4.6(优点)
  • Maven 3.6.1

技术选型需要考虑的点

  • 选择你最熟悉的技术 [最好不要超过30%的新技术]
  • 选择拥有强大社区支撑的开源技术
  • 确保技术前进步伐
  • 学会从业务端开始思考
  • 重视经验

项目初始化

  • 新建项目
  • mybatis-generator的安装配置
  • 自动生成DAO层文件
  • 跑通接口

【下方链接的主要问题就是 由于项目中的jdk和你实际安装的jdk不匹配】
{修改Project Structure → Project的版本}
{修改启动Edit的版本}
{修改setting→Build,Execution,Deployment→Compiler→Java Compiler中的Project version以及项目的Target version}

【Java异常】完美解决this version of the Java Runtime only recognizes class file versions up to xx.0异常_java runtime (class file version 61.0), this versi-CSDN博客

fileversion 55.0 this version of the Java Runtime only recognizes class file versions up to 52.0:已解决_class file version 55.0-CSDN博客

彻底解决:IDEA java: 警告: 源发行版 17 需要目标发行版 17-CSDN博客

generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
  <!-- 配置文件,放在resource目录下即可 -->
  <!--数据库驱动个人配置-->
  <classPathEntry
    location="/Users/Pluminary/.m2/repository/mysql/mysql-connector-java/8.0.18/mysql-connector-java-8.0.18.jar"/>
  <context id="MysqlTables" targetRuntime="MyBatis3">
    <property name="autoDelimitKeywords" value="true"/>
    <!--可以使用``包括字段名,避免字段名与sql保留字冲突报错-->
    <property name="beginningDelimiter" value="`"/>
    <property name="endingDelimiter" value="`"/>
    <!-- optional,旨在创建class时,对注释进行控制 -->
    <commentGenerator>
      <property name="suppressDate" value="true"/>
      <property name="suppressAllComments" value="true"/>
    </commentGenerator>
    <!--数据库链接地址账号密码-->
    <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
      connectionURL="jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull"
      userId="root"
      password="root">
      <property name="nullCatalogMeansCurrent" value="true"/>
    </jdbcConnection>
    <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
    <javaTypeResolver>
      <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>
    <!--生成Model类存放位置-->
    <javaModelGenerator targetPackage="com.imooc.mall.model.pojo"
      targetProject="src/main/java">
      <!-- 是否允许子包,即targetPackage.schemaName.tableName -->
      <property name="enableSubPackages" value="true"/>
      <!-- 是否对类CHAR类型的列的数据进行trim操作 -->
      <property name="trimStrings" value="true"/>
      <!-- 建立的Model对象是否 不可改变  即生成的Model对象不会有 setter方法,只有构造方法 -->
      <property name="immutable" value="false"/>
    </javaModelGenerator>
    <!--生成mapper映射文件存放位置-->
    <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources">
      <property name="enableSubPackages" value="true"/>
    </sqlMapGenerator>
    <!--生成Dao类存放位置-->
    <javaClientGenerator type="XMLMAPPER" targetPackage="com.imooc.mall.model.dao"
      targetProject="src/main/java">
      <property name="enableSubPackages" value="true"/>
    </javaClientGenerator>
    <!--生成对应表及类名-->
    <table schema="root" tableName="imooc_mall_cart" domainObjectName="Cart"
      enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_category" domainObjectName="Category" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_order" domainObjectName="Order" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_order_item" domainObjectName="OrderItem"
      enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_product" domainObjectName="Product" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table tableName="imooc_mall_user" domainObjectName="User" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>

  </context>
</generatorConfiguration>
application.properties
spring.datasource.name=imooc_mall_datasource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>mall</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>small</name>
    <description>small</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <!--  1.依赖mysql-connector-java  mybatis-spring-boot-starter 加入generatorConfig.xml 写下面的Plugins-->
<!-- 2. 加入imooc_mall_local.sql后 点Maven -> Plugins -> mybatis-generator -> 点击第一个 -->
<!--  通过插件生成 此时就出来了com/imooc/mall/model/dao里面所有的 和 com/imooc/mall/model/pojo里面所有的 和 mappers-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
package com.imooc.mall.controller;

import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 用户控制器
 */
@Controller
public class UserController {
    //4.返回对象的基本信息 return里面会写和service相关的  5建立Service层
    @Autowired
    UserService userService;
    @GetMapping("/test")
    @ResponseBody //返回Json格式内容
    public User personalPage(){
//  6.补全return 7告诉mapper在哪里怎么去找 去application.properties编写 mybatis.mapper-locations:......
//  去主类里编写@MapperScan(basePackages = "com.imooc.mall.model.dao") 以防找不到mapper 8.去配置端口8083
//   9.加入log4j2.xml
        return userService.getUser();
    }
}
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.User;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
}
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}

log4j2日志

  • 日志级别(优先级降低):error, warn, info, debug, trace
  • 排除Logback依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="fatal">
  <Properties>
    <Property name="baseDir" value="${sys:user.home}/Desktop/idea_Space/logs"/>
  </Properties>

  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
      <ThresholdFilter level="info" onMatch="ACCEPT"
        onMismatch="DENY"/>
      <PatternLayout
        pattern="[%d{MM:dd HH:mm:ss.SSS}] [%level] [%logger{36}] - %msg%n"/>
    </Console>

    <!--debug级别日志文件输出-->
    <RollingFile name="debug_appender" fileName="${baseDir}/debug.log"
      filePattern="${baseDir}/debug_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在debug及以上在info以下 -->
        <ThresholdFilter level="debug"/>
        <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <!-- 策略 -->
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>

    <!-- info级别日志文件输出 -->
    <RollingFile name="info_appender" fileName="${baseDir}/info.log"
      filePattern="${baseDir}/info_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在info及以上在error以下 -->
        <ThresholdFilter level="info"/>
        <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <!-- 策略 -->
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>

    <!-- error级别日志文件输出 -->
    <RollingFile name="error_appender" fileName="${baseDir}/error.log"
      filePattern="${baseDir}/error_%i.log.%d{yyyy-MM-dd}">
      <!-- 过滤器 -->
      <Filters>
        <!-- 限制日志级别在error及以上 -->
        <ThresholdFilter level="error"/>
      </Filters>
      <!-- 日志格式 -->
      <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
      <Policies>
        <!-- 每隔一天转存 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 文件大小 -->
        <SizeBasedTriggeringPolicy size="100 MB"/>
      </Policies>
    </RollingFile>
  </Appenders>
  <Loggers>
    <Root level="debug">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="debug_appender"/>
      <AppenderRef ref="info_appender"/>
      <AppenderRef ref="error_appender"/>
    </Root>

  </Loggers>
</Configuration>

AOP统一处理Web请求日志

  • 为什么需要AOP统一处理Web请求日志 [对系统健壮性的保证 创建filter]
com/imooc/mall/filter/WebLogAspect.java
package com.imooc.mall.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 打印请求和响应信息
 */
@Aspect
@Component
public class WebLogAspect {
    //生成loger类
    private final Logger log = LoggerFactory.getLogger(WebLogAspect.class);
//10.增加一个拦截点AOP
    @Pointcut("execution(public * com.imooc.mall.controller..*.*(..))")
    public void webLog(){

    }
//  10.提供请求参数
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint){
        //收到请求,记录请求内容  请求到来所作的事情
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        log.info("URL: " + request.getRequestURI().toString());
        log.info("HTTP_METHOD: " + request.getMethod());
        log.info("IP: " + request.getRemoteAddr());
        log.info("CLASS_METHOD: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("ARGS: " + Arrays.toString(joinPoint.getArgs()));
    }
//  11.返回的时候也要拦截  返回参数res 拦截点webLog
    @AfterReturning(returning = "res",pointcut = "webLog()")
    public void doAfterReturning(Object res)throws Exception{
        //处理完请求,返回内容
        log.info("RESPONSE: " + new ObjectMapper().writeValueAsString(res));
    }
}

用户模块

  • 整体介绍:知识点、功能点
    [登录、注册、重名校验、密码加密存储、Session的使用、越权校验、统一响应对象、异常枚举、Java异常体系、Postman实操、统一异常处理、更新个人信息]
  • 接口设计
  • 编码设计

API统一返回对象

package com.imooc.mall.common;

import com.imooc.mall.exception.ImoocMallExceptionEnum;

/**
 * 通用返回对象 T有可能是返回的购物车对象
 * 12.编写响应API      13创建枚举异常com/imooc/mall/exception/ImoocMallExceptionEnum.java
 */
public class ApiRestResponse<T> {
    private Integer status;
    private String msg;
    private T data;
    private static final int OK_CODE = 10000;
    private static final String OK_MSG = "SUCCESS";

    public ApiRestResponse(Integer status, String msg, T data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public ApiRestResponse(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public ApiRestResponse() {
        //默认请求信息
        this(OK_CODE, OK_MSG);
    }
    public static<T> ApiRestResponse<T> success(){
//   建立带着10000 和 SUCCESS的方法
        return new ApiRestResponse<>();
    }

    public static <T> ApiRestResponse<T> error(Integer code, String msg) {
        return new ApiRestResponse<>(code, msg);
    }
//  14.用枚举来搞错误 为了方便调试编写完后生成一个toString方法 15修改UserController的register()
    public static <T> ApiRestResponse<T> error(ImoocMallExceptionEnum ex) {
        return new ApiRestResponse<>(ex.getCode(), ex.getMsg());
    }
//   把错误[异常]创建成一个枚举类

    public static<T> ApiRestResponse<T> success(T result){
//    两个成功success的方法
        ApiRestResponse<T> response = new ApiRestResponse<>();
        response.setData(result);
        return response;
    }

    @Override
    public String toString() {
        return "ApiRestResponse{" +
                "status=" + status +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

注册接口开发

com/imooc/mall/controller/UserController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 用户控制器
 */
@Controller
public class UserController {
    //4.返回对象的基本信息 return里面会写和service相关的  5建立Service层
    @Autowired
    UserService userService;
    @GetMapping("/test")
    @ResponseBody //返回Json格式内容
    public User personalPage(){
//  6.补全return 7告诉mapper在哪里怎么去找 去application.properties编写 mybatis.mapper-locations:......
//  去主类里编写@MapperScan(basePackages = "com.imooc.mall.model.dao") 以防找不到mapper 8.去配置端口8083
//   9.加入log4j2.xml  增加aop的pom
        return userService.getUser();
    }
// 12.创造一个统一返回对象 com/imooc/mall/common/ApiRestResponse.java
    // 15.参数加在请求中所以加上@RequestParam 编写校验  16去UserService增加register接口
    @PostMapping("/register")
    @ResponseBody
    public ApiRestResponse register(@RequestParam("userName") String userName, @RequestParam("password") String password) throws ImoocMallException {
//    字符串为空 || 符合参数
        if (StringUtils.isEmpty(userName)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }if (StringUtils.isEmpty(password)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }
//   防止用户密码长度设置简单 密码长度不能少于8
        if (password.length()<8){
            return ApiRestResponse.error(ImoocMallExceptionEnum.PASSWORD_TOO_SHORT);
        }
 //18.补全操作  19进行统一处理异常[对前端安全考虑] GlobalExceptionHandler.java
        userService.register(userName, password);
        return ApiRestResponse.success();
    }
}

=========================================
http://127.0.0.1:8083/register

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Mar 06 00:06:06 CST 2024
There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'GET' not supported
    
因为注册的时候采用的是@PostMapping("/register")
单纯查询用get 往数据库写东西post
@RequestMapping("/register")是GET和POST都支持的 但是不推荐
根据不同的业务逻辑选择类型

打开postman 新建一个去查询post
POST: 127.0.0.1:8083/register?userName=mumu&password=12345678

{
    "status": 10000,
    "msg": "SUCCESS",
    "data": null
}
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.User;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
// 16.写完接口去实现接口UserServiceImpl.java
    void register(String userName, String password);
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
    @Override
    public void register(String userName, String password) throws ImoocMallException {
//    16.查询用户名是否存在, 不允许重名 用userMapper去查数据
//    但未编写功能 去手动编写UserMapper.java
        User result = userMapper.selectByName(userName);
        if (result != null){
//    17. 用户已存在 在Service层不能直接return但是controller可以直接返回
            // 创建一个异常类 com/imooc/mall/exception/ImoocMallException.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
    // 通过检测 允许写入数据库
        User user = new User();
        user.setUsername(userName);
        user.setPassword(password);
        //先判断是不是空 不是空才修改  实现完以后回到controller层进行调用
        int count = userMapper.insertSelective(user);
        if (count==0){
            throw new ImoocMallException(ImoocMallExceptionEnum.INSERT_FAILED);
        }
    }
}
com/imooc/mall/model/dao/UserMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(User record);

    int insertSelective(User record);

    User selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(User record);

    int updateByPrimaryKey(User record);

    // 16. 新增功能 去对应的UserMapper.xml进行描述 117行  BaseResultMap就是一开始自动生成的User对象
    // <include refid="Base_Column_List"/> 选取完整的User对象
    User selectByName(String userName);
}
mappers/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.mall.model.dao.UserMapper">
  <resultMap id="BaseResultMap" type="com.imooc.mall.model.pojo.User">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="personalized_signature" jdbcType="VARCHAR" property="personalizedSignature" />
    <result column="role" jdbcType="INTEGER" property="role" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
  </resultMap>
  <sql id="Base_Column_List">
    id, username, `password`, personalized_signature, `role`, create_time, update_time
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from imooc_mall_user
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    delete from imooc_mall_user
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.imooc.mall.model.pojo.User">
    insert into imooc_mall_user (id, username, `password`, 
      personalized_signature, `role`, create_time, 
      update_time)
    values (#{id,jdbcType=INTEGER}, #{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, 
      #{personalizedSignature,jdbcType=VARCHAR}, #{role,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP}, 
      #{updateTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" parameterType="com.imooc.mall.model.pojo.User">
    insert into imooc_mall_user
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">
        id,
      </if>
      <if test="username != null">
        username,
      </if>
      <if test="password != null">
        `password`,
      </if>
      <if test="personalizedSignature != null">
        personalized_signature,
      </if>
      <if test="role != null">
        `role`,
      </if>
      <if test="createTime != null">
        create_time,
      </if>
      <if test="updateTime != null">
        update_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">
        #{id,jdbcType=INTEGER},
      </if>
      <if test="username != null">
        #{username,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        #{password,jdbcType=VARCHAR},
      </if>
      <if test="personalizedSignature != null">
        #{personalizedSignature,jdbcType=VARCHAR},
      </if>
      <if test="role != null">
        #{role,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        #{updateTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.imooc.mall.model.pojo.User">
    update imooc_mall_user
    <set>
      <if test="username != null">
        username = #{username,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        `password` = #{password,jdbcType=VARCHAR},
      </if>
      <if test="personalizedSignature != null">
        personalized_signature = #{personalizedSignature,jdbcType=VARCHAR},
      </if>
      <if test="role != null">
        `role` = #{role,jdbcType=INTEGER},
      </if>
      <if test="createTime != null">
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
      <if test="updateTime != null">
        update_time = #{updateTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.imooc.mall.model.pojo.User">
    update imooc_mall_user
    set username = #{username,jdbcType=VARCHAR},
      `password` = #{password,jdbcType=VARCHAR},
      personalized_signature = #{personalizedSignature,jdbcType=VARCHAR},
      `role` = #{role,jdbcType=INTEGER},
      create_time = #{createTime,jdbcType=TIMESTAMP},
      update_time = #{updateTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
        <include refid="Base_Column_List"/>
        from imooc_mall_user
        where username = #{userName,jdbcType=VARCHAR}
  </select>
</mapper>
com/imooc/mall/exception/ImoocMallException.java
package com.imooc.mall.exception;

/**
 * 17. 统一异常(感觉像递归之前编写的枚举异常)
 */
public class ImoocMallException extends Exception{
    private final Integer code;
    private final String message;

    public ImoocMallException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public ImoocMallException(ImoocMallExceptionEnum exceptionEnum) {
        this(exceptionEnum.getCode(), exceptionEnum.getMsg());
    }

    public Integer getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

GlobalExceptionHandler编写

  • 对前端安全考虑,敏感信息不会暴露给用户
  • 抛出异常,直接转化为Json的APIResponse
  • 抛出重名异常
  • 拦截异常并且转变成APIRespond统一类型输出
com/imooc/mall/exception/GlobalExceptionHandler.java
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  
   20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }
    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }
}

对密码进行MD5保护

com/imooc/mall/util/MD5Utils.java
package com.imooc.mall.util;

import com.imooc.mall.common.Constant;
import org.apache.tomcat.util.codec.binary.Base64;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

//20.不具备解密的协议 哈希算法  因为有破解MD5的网站所以需要加盐 21com/imooc/mall/common/Constant.java
public class MD5Utils {
    public static String getMD5Str(String strValue) throws NoSuchAlgorithmException {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        return Base64.encodeBase64String(md5.digest((strValue+ Constant.SALT).getBytes()));
    }
//  用这个方法测试生成MD5的值
    public static void main(String[] args) throws NoSuchAlgorithmException {
        String md5Str = getMD5Str("12345");
        System.out.println(md5Str);
    }
}
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
}
com/imooc/mall/service/impl/UserServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.UserMapper;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import com.imooc.mall.util.MD5Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.security.NoSuchAlgorithmException;

/**
 * UserService实现类
 */
@Service
//5.重写里面的方法  @Autowired引入一个mapper去查询数据库返回真正的信息 6返回UserController补全return
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUser(){
        //通过主键来查询一个对象
        return userMapper.selectByPrimaryKey(1);
    }
    @Override
    public void register(String userName, String password) throws ImoocMallException, NoSuchAlgorithmException {
//    16.查询用户名是否存在, 不允许重名 用userMapper去查数据
//    但未编写功能 去手动编写UserMapper.java
        User result = userMapper.selectByName(userName);
        if (result != null){
//    17. 用户已存在 在Service层不能直接return但是controller可以直接返回
            // 创建一个异常类 com/imooc/mall/exception/ImoocMallException.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
    // 通过检测 允许写入数据库
        User user = new User();
        user.setUsername(userName);
//        user.setPassword(password);
        //21.重写密码
        user.setPassword(MD5Utils.getMD5Str(password));
        //先判断是不是空 不是空才修改  18实现完以后回到controller层进行调用
        int count = userMapper.insertSelective(user);
        if (count==0){
            throw new ImoocMallException(ImoocMallExceptionEnum.INSERT_FAILED);
        }
    }
}

登录功能分析

  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,会先从session中获取用户信息,然后再执行业务逻辑
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    SYSTEM_ERROR(20000,"系统异常");
com/imooc/mall/controller/UserController.java
//22登录login接口的开发 23返回UserServiceImpl写login
    @PostMapping("/login")
    @ResponseBody
    public ApiRestResponse login(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }if (StringUtils.isEmpty(password)){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//26.编写完毕login接口
        User user = userService.login(userName, password);
        //保存用户信息时,不保存密码
        user.setPassword(null);
        //把对象放入session中
        session.setAttribute(Constant.IMOOC_MALL_USER,user);
        return ApiRestResponse.success(user);
    }
com/imooc/mall/service/impl/UserServiceImpl.java
    //23.写方法判断md5与其匹配
    @Override
    public User login(String userName, String password) throws ImoocMallException {
        String md5Password = null;
        try {
            md5Password = MD5Utils.getMD5Str(password);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
//   23写方法判断md5与其匹配 UserMapper中写 login
        //25.匹配 且去接口增加
        User user = userMapper.selectLogin(userName, password);
        if (user == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_PASSWORD);
        }//能找到就返回用户 直接从上面写@Override再自动修复就可以自动生成login的接口
        //26.返回UserController写完login接口
        return user;
    }

用户模块剩余接口开发 [更新个性签名]

com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    SYSTEM_ERROR(20000,"系统异常");
}
com/imooc/mall/controller/UserController.java
//22登录login接口的开发 23返回UserServiceImpl写login
    @PostMapping("/login")
    @ResponseBody
    public ApiRestResponse login(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }
        if (StringUtils.isEmpty(password)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//26.编写完毕login接口  27更新个性签名接口
        User user = userService.login(userName, password);
        //保存用户信息时,不保存密码
        user.setPassword(null);
        //把对象放入session中 KEY
        session.setAttribute(Constant.IMOOC_MALL_USER, user);
        return ApiRestResponse.success(user);
    }

    //  27.个性签名接口 28编写UserServiceImpl的updateUserInfo方法
    @PostMapping("/user/update")
    @ResponseBody
    public ApiRestResponse updateUserInfo(HttpSession session, @RequestParam String signature) throws ImoocMallException {
        User currentUser = (User)session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }
        User user = new User();
        user.setId(currentUser.getId());
        user.setPersonalizedSignature(signature);
        //29.搞全代码
        userService.updateInformation(user);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/UserServiceImpl.java
//23.写方法判断md5与其匹配
    @Override
    public User login(String userName, String password) throws ImoocMallException {
        String md5Password = null;
        try {
            md5Password = MD5Utils.getMD5Str(password);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
//   23写方法判断md5与其匹配 UserMapper中写 login
        //25.匹配 且去接口增加
        User user = userMapper.selectLogin(userName, md5Password);
        if (user == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_PASSWORD);
        }//能找到就返回用户 直接从上面写@Override再自动修复就可以自动生成login的接口
        //26.返回UserController写完login接口
        return user;
    }
// 28写updateUserInfo方法 不需要返回任何信息 只需要提醒更行成功即可
    @Override
    public void updateInformation(User user) throws ImoocMallException {
//    更新个性签名
        int updateCount = userMapper.updateByPrimaryKeySelective(user);
        if(updateCount>1){
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }
        //快速使用@Override 快速补全接口代码 29返回UserController补全代码
    }
com/imooc/mall/service/UserService.java
package com.imooc.mall.service;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.model.pojo.User;

import java.security.NoSuchAlgorithmException;

//5.这是抽象的接口 还要让它实现 再创建一个impl 实现接口类 UserServiceImpl.java
public interface UserService {
    User getUser();
// 16.写完接口去实现接口UserServiceImpl.java
    void register(String userName, String password) throws ImoocMallException, NoSuchAlgorithmException;

    //23.写方法判断md5与其匹配
    User login(String userName, String password) throws ImoocMallException;

    // 28写updateUserInfo方法 不需要返回任何信息 只需要提醒更行成功即可
    void updateInformation(User user) throws ImoocMallException;
}

退出登录

com/imooc/mall/controller/UserController.java
//30.退出登录接口
    @PostMapping("/user/logout")
    @ResponseBody
    public ApiRestResponse logout(HttpSession session){
        session.removeAttribute(Constant.IMOOC_MALL_USER);
        return ApiRestResponse.success();
    }

管理员接口

com/imooc/mall/controller/UserController.java
    //  31.管理员登录接口 思路可以借鉴
    @PostMapping("/adminLogin")
    @ResponseBody
    public ApiRestResponse adminLogin(@RequestParam("userName") String userName, @RequestParam("password") String password, HttpSession session) throws ImoocMallException, NoSuchAlgorithmException {
        if (StringUtils.isEmpty(userName)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);
        }
        if (StringUtils.isEmpty(password)) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD_NAME);
        }//31.拿到用户名和密码的时候 对其进行校验是否为管理员
        //数据库中 role=1是普通用户   role=2是管理员用户
        // 32去serviceImpl写一个方法
        User user = userService.login(userName, password);
        //33.判断是否为管理员
//        userService.checkAdminRole(user).if
        if (userService.checkAdminRole(user)) {
            //是管理员
            //保存用户信息时,不保存密码
            user.setPassword(null);
            //把对象放入session中 KEY
            session.setAttribute(Constant.IMOOC_MALL_USER, user);
            return ApiRestResponse.success(user);
        }else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }
    //  34.创建分类接口 CategoryController
    }
com/imooc/mall/service/impl/UserServiceImpl.java
//  32.拿到用户  role=1是普通用户   role=2是管理员用户
    //33.回到Controller
    @Override
    public boolean checkAdminRole(User user){
        return user.getRole().equals(2);
    }
com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名,注册失败"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    SYSTEM_ERROR(20000,"系统异常");
}

总结用户模块

  • 重难点:统一响应对象、登录状态保持、统一异常处理
  • 常见错误:响应对象不规范、异常不统一处理

商品分类管理模块开发

  • 模块介绍
  • 编码
  • 自测
  • 总结
什么是商品分类
  • 条例清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对后台管理人员不友好,不方便管理

分类模块的主要功能

  • 分类数据的设置
  • 分类的父一级目录、递归
模块介绍
  • 接口设计
  • 表设计

开发添加分类接口part1

com/imooc/mall/controller/CategoryController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.CategoryService;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.ResponseBody;

import javax.servlet.http.HttpSession;

//  34.创建分类接口 CategoryController
@Controller
public class CategoryController {
    @Autowired
    UserService userService;
    @Autowired
    CategoryService categoryService;
    @PostMapping("admin/category/add")
    @ResponseBody
    public ApiRestResponse addCategory(HttpSession session, @RequestBody AddCategoryReq addCategoryReq) {
        //登录且必须为管理员才可以 参数中需要添加很多元素 可以使用封装来搞
        //com/imooc/mall/model/model/AddCategoryReq.java
        if (addCategoryReq.getName() == null || addCategoryReq.getType() == null || addCategoryReq.getParentId() == null || addCategoryReq.getOrderNum() == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.PARA_NOT_NULL);
        }
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null){
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole){ //35.创建一个CategoryService 分类目录Service
        //37.补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            categoryService.add(addCategoryReq);
            return ApiRestResponse.success();
        }else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }
//38.简化参数校验过程 || || || ||  @Valid  @Size(AddCategoryReq) @NotNull
        //39.增加GlobalExceptionHandler.java中的提示代码 不仅仅只提供20000 系统异常 handleMethodArg。。。
    }
}
com/imooc/mall/model/model/AddCategoryReq.java
package com.imooc.mall.model.model;

/**
 * 34.AddCategoryReq 新建一个类 供添加接口进行封装 回去引用
 */
public class AddCategoryReq {
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
}+GETTER SETTER
com/imooc/mall/service/CategoryService.java
package com.imooc.mall.service;

import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.Category;

//35.创建一个CategoryService 分类目录Service 36创建一个它的实现类CategoryServiceImpl
public interface CategoryService {
    void add(AddCategoryReq addCategoryReq);
}
com/imooc/mall/service/impl/CategoryServiceImpl.java
package com.imooc.mall.service.impl;

import com.fasterxml.jackson.databind.util.BeanUtil;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.model.AddCategoryReq;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.service.CategoryService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 36.目录分类Service实现类
 */
@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
//   去Mapper里增加selectbyName 117行
    CategoryMapper categoryMapper;
    public void add(AddCategoryReq addCategoryReq) {
        Category category = new Category();
//        category.setName(addCategoryReq.getName());
        //字段类型一样 字段名一样的话可以自动拷贝进去
        BeanUtils.copyProperties(addCategoryReq,category);
        Category categoryOld = categoryMapper.selectByName(addCategoryReq.getName());
        if (categoryOld != null){ //重名目录 不允许创建 (优化)将ImoocMallException中的extends换一个
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = categoryMapper.insertSelective(category);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.NEED_ADMIN);
        }//37.回到CategoryController.java
    }
}
mappers/CategoryMapper.xml
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_category
    where name = #{name,jdbcType=VARCHAR}
  </select>

参数校验

注解 说明
@Valid 需要验证
@NotNull 非空
@Max(value) 最大值
@Size(max, min) 字符串长度范围限制
com/imooc/mall/model/model/AddCategoryReq.java
package com.imooc.mall.model.model;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * 34.AddCategoryReq 新建一个类 供添加接口进行封装 回去引用
 */
public class AddCategoryReq {
    @Size(min=2,max=5)
    private String name;
    @NotNull
    @Max(3)
    private Integer type;
    @NotNull(message = "parentId不能为null")
    private Integer parentId;
    @NotNull
    private Integer orderNum;
    
===================================================
POST:127.0.0.1:8083/admin/category/add
Body→raw→JSON:{"name":"鸭货伴手零食","type":2,"parentId":6,"orderNum":10}

{
    "status": 20000,
    "msg": "系统异常",
    "data": null
}

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public com.imooc.mall.common.ApiRestResponse com.imooc.mall.controller.CategoryController.addCategory(javax.servlet.http.HttpSession,com.imooc.mall.model.model.AddCategoryReq): [Field error in object 'addCategoryReq' on field 'name': rejected value [鸭货伴手零食]; codes [Size.addCategoryReq.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addCategoryReq.name,name]; arguments []; default message [name],5,2]; default message [个数必须在2和5之间]] 
com/imooc/mall/exception/GlobalExceptionHandler.java
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }

    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }

    //  39.处理方法参数不合规的情况
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException: ", e);
        return handleBindingResult(e.getBindingResult());
    }
//  40.处理返回异常的ApiRespond 41去pom引入Swagger自动生成API文档
    private ApiRestResponse handleBindingResult(BindingResult result){
//  把异常处理为对外暴露的提示
        List<String> list = new ArrayList<>();
        if (result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors) { //itli快速  对着for按alt+回车 改成增强for
                String message = objectError.getDefaultMessage();
                list.add(message);
            }
        }
        if (list.size() == 0){
            return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
        } //list.toString()生成所创建的异常描述信息
        return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
    }
}

Swagger自动生成API文档

pom.xml
<!--41.导入Swagger自动生成API文档 并在main函数中加入注解 @EnableSwagger2-->
<!--42.再去创造一个config  配置文件    -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
com/imooc/mall/config/SpringFoxConfig.java
package com.imooc.mall.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SpringFoxConfig {
 //41.访问http://localhost:8083/swagger-ui.html可以看到API文档 
    //42创建ImoocMallWebMvcConfig
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕慕生鲜")
                .description("")
                .termsOfServiceUrl("")
                .build();
    }
}
com/imooc/mall/MallApplication.java
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
@EnableSwagger2
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 
 * 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
   44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    public void addResourceHandles(ResourceHandlerRegistry registry){
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}
==================================================
http://localhost:8083/swagger-ui.html

更新目录接口

com/imooc/mall/model/request/UpdateCategoryReq.java
package com.imooc.mall.model.request;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * 45.CategoryController.java中创建新增方法
 */
public class UpdateCategoryReq {
    @NotNull(message = "id不能为null")
    private Integer id;

    @Size(min=2,max=5)
    private String name;
    @Max(3)
    private Integer type;

    private Integer parentId;
    private Integer orderNum;
com/imooc/mall/controller/CategoryController.java
 //    45.创建新增接口   46在CategoryServiceImpl.java中新增更新方法
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody
    public ApiRestResponse updateCategory( @Valid @RequestBody UpdateCategoryReq updateCategoryReq, HttpSession session) {
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) {
            //补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            //46.补全接口代码
            Category category = new Category(); //复制过来
            BeanUtils.copyProperties(updateCategoryReq, category);
            categoryService.update(category);
            return ApiRestResponse.success();
        } else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }//47.为了统一接口校验管理员身份 NEED_LOGIN NEED_ADMIN
        // 创建一个com/imooc/mall/filter/AdminFilter.java
    }
com/imooc/mall/service/impl/CategoryServiceImpl.java
 @Override //47.快速生成
    // 46在CategoryServiceImpl.java中新增更新方法
    public void update(Category updateCategory){
        if (updateCategory.getName() != null){
            Category categoryOld = categoryMapper.selectByName(updateCategory.getName());
            if (categoryOld != null && !categoryOld.getId().equals(updateCategory.getId())){ //不能为空且和原来的名字不一样
                throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
            }
        }
        categoryMapper.updateByPrimaryKeySelective(updateCategory); //根据主键更新
// 不和其他名字冲突
    }
com/imooc/mall/service/CategoryService.java
package com.imooc.mall.service;

import com.imooc.mall.model.pojo.Category;
import com.imooc.mall .model.request.AddCategoryReq;

//35.创建一个CategoryService 分类目录Service 36创建一个它的实现类CategoryServiceImpl
public interface CategoryService {
    void add(AddCategoryReq addCategoryReq);

    //47.快速生成
    // 46在CategoryServiceImpl.java中新增更新方法
    void update(Category updateCategory);
}

统一校验管理员身份

com/imooc/mall/controller/CategoryController.java
//    45.创建新增接口   46在CategoryServiceImpl.java中新增更新方法
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody
    public ApiRestResponse updateCategory( @Valid @RequestBody UpdateCategoryReq updateCategoryReq, HttpSession session) {
        //对身份进行校验 用session获取当前的用户
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_LOGIN);
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) {
            //补全代码 是管理员  在上面添加@RequestBody 去postman Body->(raw/JSON)里测试接口
            //46.补全接口代码
            Category category = new Category(); //复制过来
            BeanUtils.copyProperties(updateCategoryReq, category);
            categoryService.update(category);
            return ApiRestResponse.success();
        } else {
            return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_ADMIN);
        }//47.为了统一接口校验管理员身份 NEED_LOGIN NEED_ADMIN
        // 创建一个com/imooc/mall/filter/AdminFilter.java
    }
    @ApiOperation("后台更新目录")
    @PostMapping("admin/category/update")
    @ResponseBody //没有在接口里做权限校验
    public ApiRestResponse deleteCategory(){
        return null;
    }
com/imooc/mall/filter/AdminFilter.java
package com.imooc.mall.filter;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

/**
 *
 * 47. 管理员校验过滤器  48.Admin过滤器的配置
 */
public class AdminFilter implements Filter {
    @Autowired
    UserService userService;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对身份进行校验 用session获取当前的用户
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpSession session = request.getSession();
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            PrintWriter out = new HttpServletResponseWrapper((HttpServletResponse) servletResponse).getWriter();
            out.write("{\n" //用户未登录
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
            return;
        }//校验管理员 userService写过直接引用过来
        boolean adminRole = userService.checkAdminRole(currentUser);
        if (adminRole) { //放行代码
            filterChain.doFilter(servletRequest,servletResponse);
        } else {
            PrintWriter out = new HttpServletResponseWrapper(
                    (HttpServletResponse) servletResponse).getWriter();
            out.write("{\n"
                    + "    \"status\": 10009,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
        }
    }

    @Override
    public void destroy() {

    }
}
com/imooc/mall/filter/AdminFilterConfig.java
package com.imooc.mall.filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Admin过滤器的配置
 */
@Configuration
public class AdminFilterConfig {
    @Bean
    public AdminFilter adminFilter(){
        return new AdminFilter();
    }
    @Bean(name = "adminFilterConf")
    public FilterRegistrationBean adminFilterConfig(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(adminFilter());
        filterRegistrationBean.addUrlPatterns("/admin/category/*");
        filterRegistrationBean.addUrlPatterns("/admin/product/*");
        filterRegistrationBean.addUrlPatterns("/admin/order/*");
        filterRegistrationBean.setName("adminFilterConfig");
        return filterRegistrationBean;
    }
}
com/imooc/mall/filter/AdminFilterConfig.java

删除目录接口、分页功能开发

@RequestBody用来接收http post请求的body,前端传入序列化好的json数据,后端可以解析为json对象(Content-Type需要指定为 application/json)。
@RequestParam用来接收请求url?后面的参数,或者Content-Type为multipart/form-data、application/x-www-form-urlencoded时的http body数据。
com/imooc/mall/controller/CategoryController.java
     @ApiOperation("后台删除目录")
    @PostMapping("admin/category/delete")
    @ResponseBody //没有在接口里做权限校验
    //49.编写delete接口  再去CategoryServiceImpl.java 写delete方法
    public ApiRestResponse deleteCategory(@RequestParam Integer id) {
        categoryService.delete(id);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/CategoryServiceImpl.java
//49.写delete方法     @Override快速更新service
    @Override
    public void delete(Integer id){
        Category categoryOld = categoryMapper.selectByPrimaryKey(id);
        //查不到记录,无法删除,删除失败
        if (categoryOld == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
        int count = categoryMapper.deleteByPrimaryKey(id);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
    }
com/imooc/mall/controller/CategoryController.java
 //50.写后台查询商品分类列表的接口
    @ApiOperation("后台目录列表")
    @PostMapping("admin/category/list")
    @ResponseBody //没有在接口里做权限校验
    public ApiRestResponse listCategoryForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        //51.去CategoryServiceImpl编写该接口的实现类
        PageInfo pageInfo = categoryService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }
com/imooc/mall/model/vo/CategoryVO.java
package com.imooc.mall.model.vo;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class CategoryVO {
    private Integer id;
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
    private Date createTime;
    private Date updateTime;
    private List<CategoryVO> childCategory = new ArrayList<>();
}Setter+Getter
com/imooc/mall/service/impl/CategoryServiceImpl.java
//51.创建vo[转换过后反应给前端的一个类]  pageInfo里面蕴藏着一个List<Category>
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize){
        //把分页的功能包裹在List后 而不是直接返回
        //52.引入分页查询的pom 续写分页代码
        PageHelper.startPage(pageNum,pageSize,"type,order_num");
        //53.写一个查询的mapper新的sql语句 CategoryMapper.java => List<Category> selectList();
        //54.返回Categorycontroller补全代码
        List<Category> categoryList = categoryMapper.selectList();
        PageInfo pageInfo = new PageInfo(categoryList);
        return pageInfo;
    }
pom.xml
<!--  52.引入分页的依赖 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.13</version>
        </dependency>

用户分类列表接口开发

com/imooc/mall/controller/CategoryController.java
//54.用户分类列表接口开发 去Impl补写实现类
    @ApiOperation("前台目录列表")
    @GetMapping("category/list")
    @ResponseBody
    public ApiRestResponse listCategoryForCustomer(){
        List<CategoryVO> categoryVOS = categoryService.listCategoryForCustomer();
        return ApiRestResponse.success(categoryVOS);
    }

============================================
目录列表(給用户看):127.0.0.1:8083/category/list
com/imooc/mall/service/impl/CategoryServiceImpl.java
//54.用户分类列表接口开发
    @Override
    public List<CategoryVO> listCategoryForCustomer(){
        ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
        recursivelyFindCategories(categoryVOList, 0);
        //55.去Mapper.java 和 Mapper.xml写方法
        return categoryVOList;
    }

    private void recursivelyFindCategories(List<CategoryVO> categoryVOList, Integer parentId){
        //递归获取所有子类别并组合 合成一个"目录树"
        List<Category> categoryList = categoryMapper.selectCategoriesByParentId(parentId);
        if (!CollectionUtils.isEmpty(categoryList)){
            //空 或 有无元素   itli
            for (int i = 0; i < categoryList.size(); i++) {
                Category category =  categoryList.get(i);
                CategoryVO categoryVO = new CategoryVO();
                //拷贝链接  比原来的多一个childCategory这个字段未被赋值
                BeanUtils.copyProperties(category, categoryVO);
                categoryVOList.add(categoryVO);
                //拿到childCategory字段并赋值 再从上面return categoryVOList;
                recursivelyFindCategories(categoryVO.getChildCategory(), categoryVO.getId());
            }
        }
    }
com/imooc/mall/model/dao/CategoryMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Category;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CategoryMapper {
    int deleteByPrimaryKey(Integer id);
    int insert(Category record);
    int insertSelective(Category record);
    Category selectByPrimaryKey(Integer id);
    int updateByPrimaryKeySelective(Category record);
    int updateByPrimaryKey(Category record);
    Category selectByName(String name);
    List<Category> selectList();
    List<Category> selectCategoriesByParentId(Integer parentId);
}
mappers/CategoryMapper.xml
<mapper>
 <select id="selectList" resultMap="BaseResultMap">
    select <include refid="Base_Column_List"/>
    from imooc_mall_category
  </select>
  <select id="selectCategoriesByParentId" resultMap="BaseResultMap" parameterType="int">
    select <include refid="Base_Column_List"/>
    from imooc_mall_category
    where parent_id = #{parentId}
  </select>
</mapper>

利用Redis缓存加速响应 [目录变化频繁(访问量大的) 增加效率]

pom.xml
<!--    55.导入Redis的pom文件    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
56.增加一个@EnableCaching注解让idea知道你想打开缓存功能
com/imooc/mall/MallApplication.java
package com.imooc.mall;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.mall.model.dao")
@EnableSwagger2
@EnableCaching
//56.增加注解 并去想用的前面加注解 @Cacheable(value = "listCategoryForCustomer")
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}

import org.springframework.cache.annotation.Cacheable; [对的]
import springfox.documentation.annotations.Cacheable; [错的]

com/imooc/mall/service/impl/CategoryServiceImpl.java
//这里要格外注意 不要导错包 不然运行不了redis 里面的keys会没有键值对
// import org.springframework.cache.annotation.Cacheable; [对的]
// import springfox.documentation.annotations.Cacheable; [错的]
 //54.用户分类列表接口开发
    @Override //56.在下方加想用Redis的注解  
  //57.创建一个对于缓存的配置类com/imooc/mall/config/CachingConfig.java
    @Cacheable(value = "listCategoryForCustomer")
    public List<CategoryVO> listCategoryForCustomer(){
        ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
        recursivelyFindCategories(categoryVOList, 0);
        //55.去Mapper.java 和 Mapper.xml写方法
        return categoryVOList;
    }

    private void recursivelyFindCategories(List<CategoryVO> categoryVOList, Integer parentId){
        //递归获取所有子类别并组合 合成一个"目录树"
        List<Category> categoryList = categoryMapper.selectCategoriesByParentId(parentId);
        if (!CollectionUtils.isEmpty(categoryList)){
            //空 或 有无元素   itli
            for (int i = 0; i < categoryList.size(); i++) {
                Category category =  categoryList.get(i);
                CategoryVO categoryVO = new CategoryVO();
                //拷贝链接  比原来的多一个childCategory这个字段未被赋值
                BeanUtils.copyProperties(category, categoryVO);
                categoryVOList.add(categoryVO);
                //拿到childCategory字段并赋值 再从上面return categoryVOList;
                recursivelyFindCategories(categoryVO.getChildCategory(), categoryVO.getId());
            }
        }
    }
com/imooc/mall/config/CachingConfig.java
package com.imooc.mall.config;

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

/**
 * 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口com/imooc/mall/model/vo/CategoryVO.java
 */
@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
com/imooc/mall/model/vo/CategoryVO.java
public class CategoryVO implements Serializable {
     private Integer id;
    private String name;
    private Integer type;
    private Integer parentId;
    private Integer orderNum;
    private Date createTime;
    private Date updateTime;
    private List<CategoryVO> childCategory = new ArrayList<>();
}Setter+Getter

Idea调试技巧

  • 断点统一开关
  • 条件断点
  • 单步调试
    • step into 会进入方法内部
    • step out 跳出方法
  • 表达式求值 [Evaluate Expression]

总结商品分类模块

  • 重难点:参数校验、Swagger[API文档自动]、统一鉴权[Filter过滤器]、Redis整合[Springboot整合]、调试功能
  • 常见错误:参数手动校验、项目没用Redis缓存、不善用调试

商品模块介绍

演示、数据表设计、接口设计

新增商品功能开发

com\imooc\mall\controller\ProductAdminController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import org.springframework.stereotype.Controller;

/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq
   59.需要ProductService.java
 */
@Controller
public class ProductAdminController {
    @Autowired
    ProductService productService;
    @PostMapping("admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq){
    //61.补全代码  目前图片上传还未开发成功
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }
}
com/imooc/mall/model/request/AddProductReq.java
package com.imooc.mall.model.request;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

public class AddProductReq {
    @NotNull(message = "商品名称不能为null")
    private String name;
    @NotNull(message = "商品图片不能为null")
    private String image;

    private String detail;
    @NotNull(message = "商品分类不能为null")
    private Integer categoryId;
    @NotNull(message = "商品价格不能为null")
    @Min(value = 1, message = "价格不能为null")
    private Integer price;

    @NotNull(message = "商品库存不能为null")
    @Max(value = 10000, message = "库存不能大于10000")
    private Integer stock;

    private Integer status;
}Getter+Setter
com/imooc/mall/service/ProductService.java
package com.imooc.mall.service;

import com.imooc.mall.model.request.AddProductReq;

/**
 * 59.商品Service 再创建一个实现类ProductServiceImpl
 */
public interface ProductService {

    void add(AddProductReq addProductReq);
}
com/imooc/mall/service/impl/ProductServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.AddProductReq;
import com.imooc.mall.service.ProductService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 59.商品服务实现类
 */
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    ProductMapper productMapper;//60.ProductMapper SQL通过名字查找product是否存在 151行
    @Override
    public void add(AddProductReq addProductReq){
        Product product = new Product();
        BeanUtils.copyProperties(addProductReq, product);//赋值
        //61.商品重名情况  返回ProductAdminController补全代码  
        //62.图片上传接口 ProductAdminController
        Product productOld = productMapper.selectByName(addProductReq.getName());
        if (productOld != null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = productMapper.insertSelective(product);
        if (count == 0){
            throw new ImoocMallException(ImoocMallExceptionEnum.CREATE_FAILED);
        }
    }
}

图片上传

  • 文件名UUID
  • 通用唯一识别码(Universally Unique Identifier)
  • 防止重名、防止爬图
  • 生成规则:日期和时间、MAC地址、HashCode、随机数
com/imooc/mall/controller/ProductAdminController.java
//62.图片上传接口
    @PostMapping("admin/upload/file")
    public  ApiRestResponse upload(HttpServletRequest httpServletRequest, @RequestParam("file") MultipartFile file){
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));//用文件后面的名字
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件 放在常量类中 Constant.java
        //63.application.properties配置file.upload.dir
//        new File()
        File fileDirectory = new File(Constant.FILE_UPLOAD_DIR);
        File destFile = new File(Constant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()){
            if (!fileDirectory.mkdir()){//新建文件夹
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile); //传进来的写到空的方法中去
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            return ApiRestResponse.success(getHost(new URI(httpServletRequest.getRequestURL()+""))+"/images/"+newFileName); //生成的路径IP和端口号
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }
    private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }
com/imooc/mall/common/Constant.java
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    @Value("${file.upload.dir}")
    public static String FILE_UPLOAD_DIR;
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    NAME_NOT_NULL(10010,"参数不能为空"),
    PARA_NOT_NULL(10011,"参数不能为空"),
    CREATE_FAILED(10012,"新增失败"),
    REQUEST_PARAM_ERROR(10013,"参数错误"),
    DELETE_FAILED(10014,"删除失败"),
    MKDIR_FAILED(10015,"文件夹创建失败"),
    UPLOAD_FAILED(10015,"图片上传失败"),
    SYSTEM_ERROR(20000,"系统异常");
}

资源映射开发

此时从postman中创建一个上传图片的接口
127.0.0.1:8083/admin/upload/file
选择Body→form-data
此时上传图片会报错 {
“status”: 20000,
“msg”: “系统异常”,
“data”: null
}

com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
@Component //让spring帮注入value
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    public static String FILE_UPLOAD_DIR;
    //64.为了解决上传图片系统异常报错 注入失败的原因是上方是static普通变量 set方法把静态变量赋值
    @Value("${file.upload.dir}")
    public void setFileUploadDir(String fileUploadDir){
        FILE_UPLOAD_DIR = fileUploadDir;
    }//65.打开ImoocMallWebMvcConfig 加一个映射规则
}

自定义静态资源映射目录

  • 上传图片后回显
  • 配置SpringBootWebMvcConfig
  • 静态资源到本地目录的映射
  • 演示打开图片
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}

更新(UpdateProductReq)和新增(AddProductReq)商品

  • 合并写法不可取
  • 业务逻辑清晰、独立
com/imooc/mall/model/request/UpdateProductReq.java
public class UpdateProductReq {
    @NotNull
    private Integer id; 
    private String name;
    private String image;
    private String detail;
    private Integer categoryId;
    @Min(value = 1, message = "价格不能为null")
    private Integer price;

    @Max(value = 10000, message = "库存不能大于10000")
    private Integer stock;

    private Integer status;
}
com/imooc/mall/config/ImoocMallWebMvcConfig.java
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        //65.增加一个registry  66.新增接口继续开发 ProductAdminController
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}
com/imooc/mall/controller/ProductAdminController.java
 //66. 接口 复制一个request中的AddProductReq 之后回来补全代码
    @ApiOperation("后台更新商品")
    @PostMapping("/admin/product/update")
    public ApiRestResponse updateProduct(@Valid @RequestBody UpdateProductReq updateProductReq){
        Product product = new Product();
        BeanUtils.copyProperties(updateProductReq, product);
        //67.进入ProductServiceImpl编写接口实现类
        productService.update(product);
        return ApiRestResponse.success();
    }
    //68. 搞一个删除的接口 同理也在ProductServiceImpl中写实现类 之后回来补全代码
    @ApiOperation("后台删除商品")
    @PostMapping("/admin/product/delete")
    public ApiRestResponse deleteProduct(@RequestParam Integer id){
        productService.delete(id);
        return ApiRestResponse.success();
    }com/imooc/mall/controller/ProductAdminController.java
com/imooc/mall/service/impl/ProductServiceImpl.java
//67.写updateProduct实现类 @Override自动导入
    @Override
    public void update(Product updateProduct) {
        Product productOld = productMapper.selectByName(updateProduct.getName());
        //同名且不同id,不能继续修改
        if (productOld != null && productOld.getId().equals(updateProduct.getId())) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NAME_EXISTED);
        }
        int count = productMapper.updateByPrimaryKeySelective(updateProduct);
        if (count == 0) {
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }
    }

    //68.删除实现类  @Override自动导入
    @Override
    public void delete(Integer id) {
        Product productOld = productMapper.selectByPrimaryKey(id);
        //查不到该记录,无法删除
        if (productOld == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
        int count = productMapper.deleteByPrimaryKey(id);
        if (count == 0) {
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }
    }

批量上下架

  • MyBatis遍历List
  • where语句拼接
com/imooc/mall/controller/ProductAdminController.java
//69. 批量上下架接口 同理也在ProductServiceImpl中写实现类 Napper中增加批量上下架的SQL
    // 之后回来补全代码
    @ApiOperation("后台批量上下架接口")
    @PostMapping("/admin/product/batchUpdateSellStatus")
    public ApiRestResponse batchUpdateSellStatus(@RequestParam Integer[] ids, @RequestParam Integer sellStatus){
        productService.batchUpdateSellStatus(ids,sellStatus);
        return ApiRestResponse.success();
    }
=====================================================
127.0.0.1:8083//admin/product/batchUpdateSellStatus?ids=2,3&sellStatus=0
com/imooc/mall/service/impl/ProductServiceImpl.java
//69.去ProductAdminController增加batchUpdateSellStatus接口
    @Override
    public void batchUpdateSellStatus(Integer[] ids, Integer sellStatus){
        productMapper.batchUpdateSellStatus(ids, sellStatus);
    return null;
    }
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Product record);

    int insertSelective(Product record);

    Product selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Product record);

    int updateByPrimaryKey(Product record);

    Product selectByName(String name);
    int batchUpdateSellStatus(@Param("ids") Integer[] ids, @Param("sellStatus") Integer sellStatus);

}
mappers/ProductMapper.xml
  <select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
    select <include refid="Base_Column_List"/>
    from imooc_mall_product
    where name = #{name,jdbcType = VARCHAR}
  </select>
  <update id="batchUpdateSellStatus">
    update imooc_mall_product
    set status=#{sellStatus}
    where id in
    <foreach collection="ids" close=")" item="id" open="(" separator=",">
      #{id}
    </foreach>
  </update>

后台商品列表、商品详情接口

com/imooc/mall/service/impl/ProductServiceImpl.java
 //70.后台商品列表接口
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize){
        PageHelper.startPage(pageNum, pageSize); //在mapper里写查询sql
        List<Product> products = productMapper.selectListForAdmin();
        PageInfo pageInfo = new PageInfo(products);
        return pageInfo;
    }
    //72.商品详情接口开发 再去ProductController调用
    @Override
    public Product detail(Integer id){
        Product product = productMapper.selectByPrimaryKey(id);
        return product;
    }
com/imooc/mall/controller/ProductAdminController.java
 //70.后台商品列表接口 同理也在ProductServiceImpl中写实现类 Napper中增加批量上下架的SQL 补全代码
    //71.开发与前台商品ProductController.java
    @ApiOperation("后台商品列表接口")
    @PostMapping("/admin/product/list")
    public ApiRestResponse list(@RequestParam Integer[] pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = productService.batchUpdateSellStatus(pageNum,pageSize);
        return ApiRestResponse.success(pageInfo);
    }
com/imooc/mall/controller/ProductController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.service.ProductService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 71.前台商品Controller 72新增detail接口 ProductServiceImpl.java
 */
@RestController
public class ProductController {
    @Autowired
    ProductService productService;

    @ApiOperation("商品详情")
    @GetMapping("product/detail")
    public ApiRestResponse detail(@RequestParam Integer id){
        Product product = productService.detail(id);
        return ApiRestResponse.success(product);
    }
}

前台商品列表接口part1

商品列表:搜索功能

入参判空 → 加%通配符 → like关键字

对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 所以这里要拿到某一个目录Id下的所有子目录id的List
前台:商品列表
  • 排序功能
  • MyBatis PageHelper
  • 枚举:order by [自定义]
com/imooc/mall/service/impl/ProductServiceImpl.java
//72.商品详情接口开发 再去ProductController调用
    @Override
    public Product detail(Integer id){
        Product product = productMapper.selectByPrimaryKey(id);
        return product;
    }
    //73.完成实现类
    @Override
    public PageInfo list(ProductListReq productListReq){
        //复杂查询就构建一个querry对象 ProductListQuery.java
        ProductListQuery productListQuery = new ProductListQuery();
        //☆☆搜索处理☆☆  拼接且转换成字符串去数据库查找
        if (!StringUtils.isEmpty(productListReq.getKeyword())){
            String keyword = new StringBuilder().append("%").append(productListReq.getKeyword()).append("%").toString();
            productListQuery.setKeyword(keyword);
        }
        //☆☆目录处理☆☆:如果查某个目录下的商品,不仅是需要查出该目录下的,还要把所有子目录的所有商品都查出来,所以要拿到一个目录id的List
        if (productListReq.getCategoryId() != null){
            //要拿到子目录 引用CategoryService   CategoryVO原本是给前台目录用的 需要重构一下 不是所有目录而是指定目录
            //改动代码 CategoryServiceImpl.java中的listCategoryForCustomer 传入的参数是Integer parentId
            //productListReq获取了所有根节点的list  因为点开List<CategoryVO>里面包括一个  private List<CategoryVO>递归结构
            List<CategoryVO> categoryVOList = categoryService.listCategoryForCustomer(productListReq.getCategoryId());
            ArrayList<Integer> categoryIds = new ArrayList<>(); //拿过来之后存储的list
            categoryIds.add(productListReq.getCategoryId());
            getCategoryIds(categoryVOList,categoryIds);
            productListQuery.setCategoryIds(categoryIds);
        }
        //74.排序能力 错误:前端传什么就 就传到sql中排序  这样是不安全的 要提前处理好
        //☆☆排序处理☆☆:去Constant定义支持的排序模式和手段
        String orderBy = productListReq.getOrderBy();
        if (Constant.ProductListOrderBy.PRICE_ASC_DESC.contains(orderBy)) {
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize(), orderBy);
        }else {//前端不一定包含数据的话就不排序了
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize());
        }//75.去写Mapper 最后回到ProductController写调用
        List<Product> productList = productMapper.selectList(productListQuery);
        PageInfo pageInfo = new PageInfo(productList);
        return pageInfo;
    }
    //74.写一个方法拿到所有的id 拿到参数之后往哪里存放
    private void getCategoryIds(List<CategoryVO>categoryVOList, ArrayList<Integer> categoryIds){
        for (int i = 0; i < categoryVOList.size(); i++) {
            CategoryVO categoryVO =  categoryVOList.get(i);
            if (categoryVO != null){
                categoryIds.add(categoryVO.getId());
                //递归子节点 子子节点
                getCategoryIds(categoryVO.getChildCategory(), categoryIds); //去上面调用方法 传入对象
            }
            
        }
    }
com/imooc/mall/model/query/ProductListQuery.java
package com.imooc.mall.model.query;

import java.util.List;

/**
 * 查询商品列表的Query
 */
public class ProductListQuery {
    private String keyword;
    private List<Integer> categoryIds;
}Getter+Setter
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.query.ProductListQuery;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductMapper {
    List<Product> selectListForAdmin(); //给前台用户用的
    List<Product> selectList(@Param("ids")ProductListQuery query); //给后台用户用的 Type是一个类

}
mappers/ProductMapper.xml
 <select id="selectList" resultMap="BaseResultMap"
          parameterType="com.imooc.mall.model.query.ProductListQuery">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_product
    <where>
      <if test="query.keyword != null">
        and name like #{query.keyword}
      </if>
      <if test="query.categoryIds != null">
        and category_id in
        <foreach collection="query.categoryIds" close=")" item="item" open="(" separator=",">
          #{item}
        </foreach>
      </if>
      and status = 1
    </where>
    order by update_time desc
  </select>
com/imooc/mall/model/dao/ProductMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.query.ProductListQuery;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Product record);

    int insertSelective(Product record);

    Product selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Product record);

    int updateByPrimaryKey(Product record);

    Product selectByName(String name);
    int batchUpdateSellStatus(@Param("ids") Integer[] ids, @Param("sellStatus") Integer sellStatus);

    List<Product> selectListForAdmin(); //给前台用户用的

    List<Product> selectList(@Param("query")ProductListQuery query); //给后台用户用的 Type是一个类

}
com/imooc/mall/controller/ProductController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.ProductListReq;
import com.imooc.mall.service.ProductService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 71.前台商品Controller 72新增detail接口 ProductServiceImpl.java
 */
@RestController
public class ProductController {
    @Autowired
    ProductService productService;

    @ApiOperation("商品详情")
    @GetMapping("product/detail")
    public ApiRestResponse detail(@RequestParam Integer id){
        Product product = productService.detail(id);
        return ApiRestResponse.success(product);
    }
    //73.开发前台商品列表 开一个ProductListReq.java 去ProductServiceImpl
    @ApiOperation("商品详情")
    @GetMapping("product/list")
    public ApiRestResponse list(ProductListReq productListReq){
        PageInfo list = productService.list(productListReq);
        return ApiRestResponse.success(list);
    }
}

总结商品模块

  • 重难点:商品的搜索[like]、排序[枚举+自定义]、目录查询[所有目录id都查到 再用查询列表方式]
  • 常见错误:更新和新增放在同一个接口、排序字段不用枚举

购物车模块介绍

业务流程
  • 添加商品到购物车 → 商品是否在售、是否有库存
    • →[否] 提示用户
    • →[是] 该商品之前就在购物车里
      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
com/imooc/mall/controller/CartController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 76.购物车Controller  77.用一个通用UserFilter逻辑
 */
@Controller
@RequestMapping("/cart")
public class CartController {//因为放在url中 要加数据绑定 通过HttpSession获取用户信息太麻烦 用一个通用filter逻辑
    @PostMapping("/add")
    public ApiRestResponse add(@RequestParam Integer productId, @RequestParam Integer count){
        return null;
    }
}
========================================================
127.0.0.1:8083/cart/add?productId=22&count=1
com/imooc/mall/filter/UserFilter.java
package com.imooc.mall.filter;

import com.imooc.mall.common.Constant;
import com.imooc.mall.model.pojo.User;
import com.imooc.mall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

/**
 *
 * 77.用户过滤器 希望把用户信息currentUser保存下来
 */
public class UserFilter implements Filter {
    public static User currentUser;
    @Autowired
    UserService userService;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对身份进行校验 用session获取当前的用户
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpSession session = request.getSession();
        currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            PrintWriter out = new HttpServletResponseWrapper((HttpServletResponse) servletResponse).getWriter();
            out.write("{\n" //用户未登录
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            out.flush();
            out.close();
            return;
        }
        //78.续写过滤器链条 写好后对用户链条进行config配置 UserFilterConfig.java
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}
com/imooc/mall/config/UserFilterConfig.java
package com.imooc.mall.config;

import com.imooc.mall.filter.UserFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 78.对调用接口时对哪个类进行拦截 过滤器的配置 79创建cartService
 */
@Configuration
public class UserFilterConfig {
    @Bean
    public UserFilter userFilter(){
        return new UserFilter();
    }
    @Bean(name = "userFilterConf")
    public FilterRegistrationBean adminFilterConfig(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(userFilter());
        //下面的是校验与拦截
        filterRegistrationBean.addUrlPatterns("/cart/*"); //CartController.java
        filterRegistrationBean.addUrlPatterns("/order/*");
        filterRegistrationBean.setName("userFilterConf");
        return filterRegistrationBean;
    }
}
com/imooc/mall/service/impl/CartServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.vo.CartVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 80.购物车Service实现类 其中List<>中间是需要包括 商品id 图片 名字 商品选中 数量信息
 * 81.创建一个VO增加那些属性放在<>中 是返回给前端组合后的对象
 */
@Service
public class CartServiceImpl {
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CategoryMapper categoryMapper;

    public List<CartVO> add(Integer userId, Integer productId, Integer count) {
        validProduct(productId, count);

    }

    private void validProduct(Integer productId, Integer count) {
        Product product = productMapper.selectByPrimaryKey(productId);
        //判断商品是否存在,商品是否上架
        if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
            //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
        }
        //判断商品库存 如果要买的比库存多 就买不了
        if (count > product.getStock()) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
        }
    }
}
com/imooc/mall/common/Constant.java
    //83.创建商品上下架的状态 创建CartMapper[Cart selectCartByUserIdAndProductId] 写sql 
    //84再去CartServiceImpl
    public interface SaleStatus{
        int NOT_SALE = 0; //商品下架状态
        int SALE = 1; //商品上架状态
    }
@Service
public class CartServiceImpl implements CartService {
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CategoryMapper categoryMapper;
    @Autowired
    CartMapper cartMapper;
@Override
    public List<CartVO> add(Integer userId, Integer productId, Integer count) {
        validProduct(productId, count);
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        //84.补全代码
        if (cart==null){
            //这个商品之前不在购物车里,需要新增一个记录
            cart = new Cart();
            cart.setProductId(productId);
            cart.setUserId(userId);
            cart.setQuantity(count);
            cart.setSelected(Constant.Cart.CHECKED);
            cartMapper.insertSelective(cart);
        }else {
            //这个商品已经在购物车里了,数量相加
            count = cart.getQuantity() + count;
            Cart cartNew = new Cart();
            cartNew.setQuantity(count);
            cartNew.setId(cart.getId());
            cartNew.setProductId(cart.getProductId());
            cartNew.setUserId(cart.getUserId());
            //无论是否想买都选中
            cartNew.setSelected(Constant.Cart.CHECKED);
            cartMapper.updateByPrimaryKeySelective(cartNew);
        }
        //85.去CartController补全代码逻辑 返回购物车列表哦~ 86.CartMapper.java
        return null;
    }

    private void validProduct(Integer productId, Integer count) {
        Product product = productMapper.selectByPrimaryKey(productId);
        //判断商品是否存在,商品是否上架
        if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
            //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
        }
        //判断商品库存 如果要买的比库存多 就买不了
        if (count > product.getStock()) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
        }
    }
}
com/imooc/mall/model/dao/CartMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Cart;
import com.imooc.mall.model.vo.CartVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Repository
public interface CartMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Cart record);

    int insertSelective(Cart record);

    Cart selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Cart record);

    int updateByPrimaryKey(Cart record);

    //86.选中列表的方法 传入的参数是userId
    List<CartVO> selectList(@Param("userId") Integer userId);
    Cart selectCartByUserIdAndProductId(@Param("userId") Integer userId, @Param("productId")Integer productId);
}
mappers/CartMapper.xml
 <select id="selectList" resultType="com.imooc.mall.model.vo.CartVO" parameterType="java.lang.Integer">
    select
    c.id as id,
    p.id as productId,
    c.user_id as userId,
    c.quantity as quantity,
    c.selected as selected,
    p.price as price,
    p.name as productName,
    p.image as productImage
    from imooc_mall_cart c
    left join imooc_mall_product p on p.id = c.product_id
    where c.user_id = #{userId}
    and p.status = 1
  </select>
com/imooc/mall/controller/CartController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 76.购物车Controller  77.用一个通用User_filter逻辑
 */
@RestController //404的错误① 返回的json格式 要用RestController
@RequestMapping("/cart")
public class CartController {//因为放在url中 要加数据绑定 通过HttpSession获取用户信息太麻烦 用一个通用filter逻辑
    @Autowired
    CartService cartService;
    @PostMapping("/list")
    @ApiOperation("购物车列表")
    public ApiRestResponse list(){
        //86.去写service的list接口 CartMapper中写选中列表的方法
        //内部获取用户Id,防止横向越权
        List<CartVO> cartList = cartService.list(UserFilter.currentUser.getId());
        return ApiRestResponse.success();
    }

    @PostMapping("/add")
    @ApiOperation("添加商品到购物车")
    public ApiRestResponse add(@RequestParam Integer productId, @RequestParam Integer count){
        //85.补全完逻辑代码 87在下面补全代码 List<CartVO> cartVOList =
        List<CartVO> cartVOList = cartService.add(UserFilter.currentUser.getId(), productId, count);
        return ApiRestResponse.success(cartVOList);
    }
}

更新、删除购物车接口

com/imooc/mall/controller/CartController.java
@PostMapping("/update")
    @ApiOperation("更新购物车")
    public ApiRestResponse update(@RequestParam Integer productId, @RequestParam Integer count){
        //86.去service层中写更新方法 CartServiceImpl.java
        List<CartVO> cartVOList = cartService.update(UserFilter.currentUser.getId(), productId, count);
        return ApiRestResponse.success(cartVOList);
    }

    @PostMapping("/delete")
    @ApiOperation("删除保护购物车")
    public ApiRestResponse delete(@RequestParam Integer productId){
        //87.删除保护购物车方法
        //不能传入userId, cartId, 否则可以删除别人的购物车
        List<CartVO> cartVOList = cartService.delete(UserFilter.currentUser.getId(), productId);
        return ApiRestResponse.success(cartVOList);
    }
=====================================================================
127.0.0.1:8083/cart/delete?productId=22
com/imooc/mall/service/impl/CartServiceImpl.java
 @Override
    public List<CartVO> update(Integer userId, Integer productId, Integer count){
        //86.去service层中写更新方法
        validProduct(productId, count);
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //这个商品之前不在购物车里,无法更新
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }else {
            //这个商品已经在购物车里了,则更新数量
            Cart cartNew = new Cart();
            cartNew.setQuantity(count);
            cartNew.setId(cart.getId());
            cartNew.setProductId(cart.getProductId());
            cartNew.setUserId(cart.getUserId());
            //无论是否想买都选中
            cartNew.setSelected(Constant.Cart.CHECKED);
            cartMapper.updateByPrimaryKeySelective(cartNew);
        }
        return this.list(userId);
    }

    @Override
    public List<CartVO> delete(Integer userId, Integer productId){
        //86.去service层中写更新方法
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //这个商品之前不在购物车里,无法更新
            throw new ImoocMallException(ImoocMallExceptionEnum.DELETE_FAILED);
        }else {
            //这个商品已经在购物车里了,则可以删除
            cartMapper.deleteByPrimaryKey(cart.getId());
        }
        return this.list(userId);
    }
======================================================================
127.0.0.1:8083/cart/update?productId=22&count=1
com/imooc/mall/service/CartService.java
package com.imooc.mall.service;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.request.AddCategoryReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.model.vo.CategoryVO;

import java.util.List;

/**
 * 79.购物车service 增加CartServiceImpl
 */
public interface CartService {

    //87.补全list实现类
    List<CartVO> list(Integer userId);

    List<CartVO> add(Integer userId, Integer productId, Integer count);

    List<CartVO> update(Integer userId, Integer productId, Integer count);

    List<CartVO> delete(Integer userId, Integer productId);
}

浅谈@RequestParam、@RequestBody、@PathVariable - 知乎 (zhihu.com)

选中购物车相关接口

com/imooc/mall/controller/CartController.java
 @PostMapping("/select")
    @ApiOperation("选择/不选择购物车的某商品")
    public ApiRestResponse select(@RequestParam Integer productId, @RequestParam Integer selected){
        //88.选/不选购物车某商品
        //不能传入userId, cartId, 否则可以删除别人的购物车
        List<CartVO> cartVOList = cartService.selectOrNot(UserFilter.currentUser.getId(), productId,selected);
        return ApiRestResponse.success(cartVOList);
    }

    @PostMapping("/selectAll")
    @ApiOperation("全选择/全不选择购物车的某商品")
    public ApiRestResponse selectAll(@RequestParam Integer selected){
        //89.全选/全不选购物车某商品
        //不能传入userId, cartId, 否则可以删除别人的购物车
////90.订单模块接口编写 91创建OrderController.java  OrderService.java  OrderServiceImpl.java
        List<CartVO> cartVOList = cartService.selectAllOrNot(UserFilter.currentUser.getId(), selected);
        return ApiRestResponse.success(cartVOList);
    }
com/imooc/mall/service/impl/CartServiceImpl.java
    @Override
    public List<CartVO> selectOrNot(Integer userId, Integer productId, Integer selected){
        Cart cart = cartMapper.selectCartByUserIdAndProductId(userId, productId);
        if (cart==null){
            //88.这个商品之前不在购物车里,无法选择/不选中
            throw new ImoocMallException(ImoocMallExceptionEnum.UPDATE_FAILD);
        }else {
            //这个商品已经在购物车里了,则可以选中/不选中
            cartMapper.selectOrNot(userId, productId, selected);
        }//返回购物车列表
        return this.list(userId);
    }

    @Override
    public List<CartVO> selectAllOrNot(Integer userId, Integer selected){
        //89.这里填null是对mapper中的sql进行等于null判断的语句 改变选中状态
        cartMapper.selectOrNot(userId, null, selected);
        return this.list(userId);
    }
com/imooc/mall/service/CartService.java
package com.imooc.mall.service;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.model.pojo.Category;
import com.imooc.mall.model.request.AddCategoryReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.model.vo.CategoryVO;

import java.util.List;

/**
 * 79.购物车service 增加CartServiceImpl
 */
public interface CartService {

    //87.补全list实现类
    List<CartVO> list(Integer userId);

    List<CartVO> add(Integer userId, Integer productId, Integer count);

    List<CartVO> update(Integer userId, Integer productId, Integer count);

    List<CartVO> delete(Integer userId, Integer productId);

    List<CartVO> selectOrNot(Integer userId, Integer productId, Integer selected);

    List<CartVO> selectAllOrNot(Integer userId, Integer selected);
}

总结购物车模块

  • 重难点
    • MyBatis返回非标准对象、后期计算单样商品的总价 [数据库无法直接查到]
    • 添加商品到购物车时,根据是否已经存在该商品,有不同逻辑
  • 常见错误:不做越权判断 [不允许前端传入Id]

创建订单接口 - 主流程框架搭建

  • 登录 → 浏览商品 → 加入购物车 → 下单
    • ​ → 取消订单
    • ​ → 扫码支付 → 发货 → 收获 → 订单完结

生成订单 —— 用户下单

  • 入参
  • 从购物车中查找已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖,扣库存
  • 数据库事务(实操演示效果)
  • 删除购物车中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每个商品到order_item表
  • 进入Coding阶段
com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        return null;
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.dao.CartMapper;
import com.imooc.mall.model.dao.CategoryMapper;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Cart;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import com.imooc.mall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;

    public String create(CreateOrderReq createOrderReq) {
        //拿到用户ID
        Integer userId = UserFilter.currentUser.getId();
        //从购物车查找已经勾选的商品 CartVO里面包含着任何信息
        List<CartVO> cartVOList = cartService.list(userId);
        //勾选的单独拿出来
        ArrayList<CartVO> cartVOListTemp = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO = cartVOList.get(i);
            if (cartVO.getSelected().equals(Constant.Cart.CHECKED)) {
                cartVOListTemp.add(cartVO);
            }
        }
        cartVOList = cartVOListTemp;
        //如果购物车已勾选的为空,报错
        if (CollectionUtils.isEmpty(cartVOList)){
            throw new ImoocMallException(ImoocMallExceptionEnum.CART_EMPTY);
        }
    }
    //判断商品是否存在、上下架状态、库存
    //把购物车对象转换为订单item对象
    //扣库存
    //把购物车中的已勾选商品删除
    //生成订单
    //生成订单号,有独立的规则
    //循环保存每个商品的order_item表
    //把结果返回
}
}
com/imooc/mall/exception/ImoocMallExceptionEnum.java
package com.imooc.mall.exception;

/**
 * 异常枚举
 */
//13.编写异常枚举 注意类是enum噢  14返回ApiRestResponse
public enum ImoocMallExceptionEnum {
//  正确状态码是10000 这个错误的就10001 变红是因为没构造函数
    NEED_USER_NAME(10001,"用户名不能为空"),
    NEED_PASSWORD_NAME(10002,"密码不能为空"),
    PASSWORD_TOO_SHORT(10003,"密码长度不能小于8位"),
    NAME_EXISTED(10004,"不允许重名"),
    INSERT_FAILED(10005,"插入失败,请重试"),
    WRONG_PASSWORD(10006,"密码错误"),
    NEED_LOGIN(10007,"用户未登录"),
    UPDATE_FAILD(10008,"更新失败"),
    NEED_ADMIN(10009,"无管理员权限"),
    NAME_NOT_NULL(10010,"参数不能为空"),
    CREATE_FAILED(10011,"新增失败"),
    REQUEST_PARAM_ERROR(10012,"参数错误"),
    DELETE_FAILED(10013,"删除失败"),
    MKDIR_FAILED(10014,"文件夹创建失败"),
    UPLOAD_FAILED(10015,"图片上传失败"),
    NOT_SALE(10016,"商品状态不可售"),
    NOT_ENOUGH(10017,"商品库存不足"),
    CART_EMPTY(10018, "购物车已勾选的商品为空"),
    SYSTEM_ERROR(20000,"系统异常");


    //异常码
    Integer code;
    //异常信息
    String msg;

    ImoocMallExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

创建订单 —— 对象转换与扣库存

com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    @ApiOperation("创建订单")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        //95.调用orderService
        String orderNo = orderService.create(createOrderReq);
        return ApiRestResponse.success(orderNo);
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
package com.imooc.mall.service.impl;

import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.dao.*;
import com.imooc.mall.model.pojo.Order;
import com.imooc.mall.model.pojo.OrderItem;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import com.imooc.mall.service.OrderService;
import com.imooc.mall.util.OrderCodeFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CartMapper cartMapper;
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    OrderItemMapper orderItemMapper;
    @Override
    public String create(CreateOrderReq createOrderReq) {
        //拿到用户ID
        Integer userId = UserFilter.currentUser.getId();
        //从购物车查找已经勾选的商品 CartVO里面包含着任何信息
        List<CartVO> cartVOList = cartService.list(userId);
        //勾选的单独拿出来
        ArrayList<CartVO> cartVOListTemp = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO = cartVOList.get(i);
            if (cartVO.getSelected().equals(Constant.Cart.CHECKED)) {
                cartVOListTemp.add(cartVO);
            }
        }
        cartVOList = cartVOListTemp;
        //如果购物车已勾选的为空,报错
        if (CollectionUtils.isEmpty(cartVOList)){
            throw new ImoocMallException(ImoocMallExceptionEnum.CART_EMPTY);
        }
        //判断商品是否存在、上下架状态、库存
        validSaleStatusAndStock(cartVOList);
        //把购物车对象转换为订单item对象
        List<OrderItem> orderItemList = cartVOListToOrderItemList(cartVOList);
        //扣库存
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            Product product = productMapper.selectByPrimaryKey(orderItem.getProductId());
            int stock = product.getStock() - orderItem.getQuantity();
            if (stock < 0){
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
            product.setStock(stock);
            productMapper.updateByPrimaryKeySelective(product);
        }
        //把购物车中的已勾选商品删除
        cleanCart(cartVOList);
        //生成订单
        Order order = new Order();
        //93.创建一个Util/OrderCodeFactory
        //生成订单号,有独立的规则
        String orderNo = OrderCodeFactory.getOrderCode(Long.valueOf(userId));
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalPrice(totalPrice(orderItemList));
        order.setReceiverName(createOrderReq.getReceiverName());
        order.setReceiverMobile(createOrderReq.getReceiverMobile());
        order.setReceiverAddress(createOrderReq.getReceiverAddress());
        order.setOrderStatus(Constant.OrderStatusEnum.NOT_PAID.getCode()); //94.去Constant定义订单状态
        //循环保存每个商品的order_item表
        order.setPostage(0);
        order.setPaymentType(1);
        //插入到Order表
        orderMapper.insertSelective(order);
        //循环保存每个商品到order_item表
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            orderItem.setOrderNo(order.getOrderNo());
            orderItemMapper.insertSelective(orderItem);
        }
        //把结果返回
        return orderNo;
        //95.回到Controller调用
    }
//92.生成相似代码并改造
    private void validSaleStatusAndStock(List<CartVO> cartVOList) {
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            Product product = productMapper.selectByPrimaryKey(cartVO.getProductId());
            //判断商品是否存在,商品是否上架
            if (product == null || product.getStatus().equals(Constant.SaleStatus.NOT_SALE)) {
                //82. 1是上架 1不明确要形成代码让顾客知道 Constant.java
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
            }
            //判断商品库存 如果要买的比库存多 就买不了
            if (cartVO.getQuantity() > product.getStock()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
        }
    }
    private List<OrderItem> cartVOListToOrderItemList(List<CartVO> cartVOList) {
        List<OrderItem> orderItemList = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            OrderItem orderItem = new OrderItem();
            orderItem.setProductId(cartVO.getProductId());
            //记录商品快照信息
            orderItem.setProductName(cartVO.getProductName());
            orderItem.setProductImg(cartVO.getProductImage());
            orderItem.setUnitPrice(cartVO.getPrice());
            orderItem.setQuantity(cartVO.getQuantity());
            orderItem.setTotalPrice(cartVO.getTotalPrice());
            orderItemList.add(orderItem);
        }
        return orderItemList;
    }
    private void cleanCart(List<CartVO> cartVOList){
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            cartMapper.deleteByPrimaryKey(cartVO.getId());

        }
    }
    private Integer totalPrice(List<OrderItem> orderItemList){
        Integer totalPrice = 0;
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}
com/imooc/mall/util/OrderCodeFactory.java
package com.imooc.mall.util;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 描述:     生成订单No工具类
 */
public class OrderCodeFactory {

    /**
     * 订单类别头
     */
    private static final String ORDER_CODE = "1";
    /**
     * 随机编码
     */
    private static final int[] r = new int[]{7, 9, 6, 2, 8, 1, 3, 0, 5, 4};
    /**
     * 用户id和随机数总长度
     */
    private static final int maxLength = 5;

    /**
     * 更具id进行加密+加随机数组成固定长度编码
     */
    private static String toCode(Long id) {
        String idStr = id.toString();
        StringBuilder idSb = new StringBuilder();
        for (int i = idStr.length() - 1; i >= 0; i--) {
            idSb.append(r[idStr.charAt(i) - '0']);
        }
        return idSb.append(getRandom(maxLength - idStr.length())).toString();
    }

    /**
     * 生成时间戳
     */
    private static String getDateTime() {
        DateFormat sdf = new SimpleDateFormat("HHmmss");
        return sdf.format(new Date());
    }

    /**
     * 生成固定长度随机码
     *
     * @param n 长度
     */
    private static long getRandom(long n) {
        long min = 1, max = 9;
        for (int i = 1; i < n; i++) {
            min *= 10;
            max *= 10;
        }
        long rangeLong = (((long) (new Random().nextDouble() * (max - min)))) + min;
        return rangeLong;
    }

    /**
     * 生成不带类别标头的编码
     */
    private static synchronized String getCode(Long userId) {
        userId = userId == null ? 10000 : userId;
        return getDateTime() + toCode(userId);
    }

    /**
     * 生成订单单号编码
     */
    public static String getOrderCode(Long userId) {
        return ORDER_CODE + getCode(userId);
    }
}
com/imooc/mall/common/Constant.java
package com.imooc.mall.common;

import com.google.common.collect.Sets;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Set;


/**
 * 21.SALT常量值 写完以后去UserServiceImpl重写密码代码
 */
@Component //让spring帮注入value
public class Constant {
    public static final String SALT = "aSp[PCx,aw.xq246}";
    public static final String IMOOC_MALL_USER = "imooc_mall_user";
    public static String FILE_UPLOAD_DIR;
    //64.为了解决上传图片系统异常报错 注入失败的原因是上方是static普通变量 set方法把静态变量赋值
    @Value("${file.upload.dir}")
    public void setFileUploadDir(String fileUploadDir){
        FILE_UPLOAD_DIR = fileUploadDir;
    }//65.打开ImoocMallWebMvcConfig 加一个映射规则

    //74.排序处理 去Constant定义支持的排序模式和手段
    public interface ProductListOrderBy{
        Set<String> PRICE_ASC_DESC = Sets.newHashSet("price desc","price asc");
    }
    //83.创建商品上下架的状态 创建CartMapper[Cart selectCartByUserIdAndProductId] 写sql 84再去CartServiceImpl
    public interface SaleStatus{
        int NOT_SALE = 0; //商品下架状态
        int SALE = 1; //商品上架状态
    }
    public interface Cart{
        int UN_CHECKED = 0; //购物车未选中状态
        int CHECKED = 1; //购物车选中状态
    }
    public enum OrderStatusEnum{
        CANCELED(0, "用户已取消"),
        NOT_PAID(10, "未付款"),
        PAID(20, "已付款"),
        DELIVERED(30, "已发货"),
        ;

        private String value;
        private int code;
        //94.去Constant定义订单状态
        OrderStatusEnum(int code,String value){
            this.value = value;
            this.code = code;
        }
        public static OrderStatusEnum codeOf(int code){
            for (OrderStatusEnum orderStatusEnum : values()){
                if (orderStatusEnum.getCode() == code){
                    return orderStatusEnum;
                }
            }
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ENUM);
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }
    }
}

添加数据库事务

//95.写数据库事务代码 @Transactional (要么全对全要 要么全不要)
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String create(CreateOrderReq createOrderReq) {
     ......
    }

订单详情

com/imooc/mall/service/impl/OrderServiceImpl.java
/* 95.继续实现Service实现类 拷贝一个Order成OrderVo 新增两个属性
        private String orderStatusName;
        private List<OrderItemVo> orderItemVoList;
        因为没有<OrderItemVo>要根据需求去创建
        拷贝OrderItem成OrderItemVO 更改属性
        之后回来补写代码
     */
    public OrderVO detail(String orderNo){

    }
com/imooc/mall/model/vo/OrderItemVO.java
public class OrderItemVO {
    private Integer id;

    private String orderNo;

    private Integer productId;

    private String productName;

    private String productImg;

    private Integer unitPrice;

    private Integer quantity;

    private Integer totalPrice;
com/imooc/mall/model/vo/OrderVO.java
public class OrderVO {
    private Integer id;

    private String orderNo;

    private Integer userId;

    private Integer totalPrice;

    private String receiverName;

    private String receiverMobile;

    private String receiverAddress;

    private Integer orderStatus;

    private Integer postage;

    private Integer paymentType;

    private Date deliveryTime;

    private Date payTime;

    private Date endTime;

    private Date createTime;

    private Date updateTime;
订单状态
  • 0 用户已取消
  • 10 未付款(下单后的初始状态)
  • 20 已付款
  • 30 已发货
  • 40 交易完成
com/imooc/mall/service/impl/OrderServiceImpl.java
 /* 95.继续实现Service实现类 拷贝一个Order成OrderVo 新增两个属性
        private String orderStatusName;
        private List<OrderItemVo> orderItemVoList;
        因为没有<OrderItemVo>要根据需求去创建
        拷贝OrderItem成OrderItemVO 更改属性
        之后回来补写代码
     */
   @Override
    public OrderVO detail(String orderNo){
        //96.去创建mapper的方法后回来补写代码 97写订单controller
        Order order = orderMapper.selectByOrderNo(orderNo);
        //订单不存在,则报错
        if (order == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //订单存在,需要判断所属
        Integer userId = UserFilter.currentUser.getId();
        if (!order.getUserId().equals(userId)) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        OrderVO orderVO = getOrderVo(order);
        return orderVO;
    }

    private OrderVO getOrderVo(Order order) {
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(order,orderVO);
        //获取订单对应的orderItemVOList
        List<OrderItem> orderItemList = orderItemMapper.selectByOrderNo(order.getOrderNo());
        //OrderItem和OrderItem相比少了几个字段
        List<OrderItemVO> orderItemVOList = new ArrayList<>();
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem = orderItemList.get(i);
            OrderItemVO orderItemVO = new OrderItemVO();
            BeanUtils.copyProperties(orderItem, orderItemVO);
            orderItemVOList.add(orderItemVO);
        }
        orderVO.setOrderItemVOList(orderItemVOList);
        //com/imooc/mall/common/Constant.java的列举枚举
        orderVO.setOrderStatusName(Constant.OrderStatusEnum.codeOf(orderVO.getOrderStatus()).getValue());
        return orderVO;
    }
com/imooc/mall/model/dao/OrderMapper.java
package com.imooc.mall.model.dao;

import com.imooc.mall.model.pojo.Order;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(Order record);

    int insertSelective(Order record);

    Order selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(Order record);

    int updateByPrimaryKey(Order record);

    Order selectByOrderNo(String orderNo);
}

=====================================================
 <select id="selectByOrderNo" resultMap="BaseResultMap" parameterType="java.lang.String">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    where order_no = #{orderNo}
  </select>
com/imooc/mall/controller/OrderController.java
package com.imooc.mall.controller;

import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.OrderVO;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 91.订单Controller 新建一个根据pojo的Order → CreateOrderReq 新建一个OrderService
 */
@RestController
public class OrderController {
    @Autowired
    OrderService orderService;

    @PostMapping("order/create")
    @ApiOperation("创建订单")
    public ApiRestResponse create(@RequestBody CreateOrderReq createOrderReq){
        //95.调用orderService
        String orderNo = orderService.create(createOrderReq);
        return ApiRestResponse.success(orderNo);
    }

    @GetMapping("order/detail")
    @ApiOperation("前台订单详情")
    public ApiRestResponse detail(@RequestParam String orderNo){
        //95.调用orderService  Service层具体实现
        OrderVO orderVO = orderService.detail(orderNo);
        return ApiRestResponse.success(orderVO);
    }
}
==========================================
生成订单详情 GET:127.0.0.1:8083/order/detail?orderNo=100233359639

订单列表

com/imooc/mall/controller/OrderController.java
//97.写一个订单列表controller
    @GetMapping("order/list")
    @ApiOperation("前台订单列表")
    public ApiRestResponse list(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        //95.调用list   Service  Service层具体实现
        PageInfo pageInfo = orderService.listForCustomer(pageNum, pageSize);
        return  ApiRestResponse.success(pageInfo);
    }
===============================================
订单列表:GET:127.0.0.1:8083/order/list?pageNum=1&pageSize=10
com/imooc/mall/service/impl/OrderServiceImpl.java
  //97.前台订单实现类 搞个OrderMapper中的selectForCustomer
    @Override
    public PageInfo listForCustomer(Integer pageNum, Integer pageSize){
        Integer userId = UserFilter.currentUser.getId();
        PageHelper.startPage(pageNum, pageSize);
        List<Order> orderList = orderMapper.selectForCustomer(userId);
        List<OrderVO> orderVOList = orderListToOrderVOList(orderList);
        //新建一个pageinfo返回 方便前端查看
        PageInfo pageInfo = new PageInfo<>(orderList);
        pageInfo.setList(orderVOList);
        return pageInfo;
    }

    private List<OrderVO> orderListToOrderVOList(List<Order> orderList) {
        List<OrderVO> orderVOList = new ArrayList<>();
        for (int i = 0; i < orderList.size(); i++) {
            Order order =  orderList.get(i);
            OrderVO orderVO = getOrderVo(order);
            orderVOList.add(orderVO);
        }
        return  orderVOList;
    }
com/imooc/mall/model/dao/OrderMapper.java
 List<Order> selectForCustomer(Integer userId);
===============================================
mappers/OrderMapper.xml
  <select id="selectForCustomer" resultMap="BaseResultMap" parameterType="integer">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    where user_id = #{userId}
    order by create_time desc
  </select>

取消订单

com/imooc/mall/controller/OrderController.java
//98.写一个订单取消controller
    @PostMapping("order/cancel")
    @ApiOperation("前台取消订单")
    public ApiRestResponse cancel(@RequestParam String orderNo){
        orderService.cancel(orderNo);
        return  ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
//98.写一个订单取消实现类
    @Override
    public void cancel(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null){
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //验证用户身份
        //订单存在,需要判断所属
        Integer userId = UserFilter.currentUser.getId();
        if (!order.getUserId().equals(userId)) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        //没有付款才可以取消
        if (order.getOrderStatus().equals(Constant.OrderStatusEnum.NOT_PAID.getCode())) {
            order.setOrderStatus(Constant.OrderStatusEnum.CANCELED.getCode());
            order.setEndTime(new Date());
            //更新状态
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

二维码接口开发

在线生成 QR Code (oschina.net)
Springboot 上传图片到项目路径下不能访问,需要重启_springboot能上传图片到win11中但无法访问图片-CSDN博客

com/imooc/mall/controller/OrderController.java
//99.生成支付二维码
    @GetMapping("order/qrcode")
    @ApiOperation("前台取消订单")
    public ApiRestResponse qrcode(@RequestParam String orderNo){
        orderService.cancel(orderNo);
        return  ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
 //99.生成支付二维码 pom.xml生成新的二维码依赖 util新建一个QRCodeGenerator类
       @Override
    public String qrcode(String orderNo) {
        //得到请求相关的信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();//不能直接拿到内网的信息 要经过多层转发才可以
        //去application.properties配置一下可以访问的ip  去上面String ip
        String address = ip + ":" + request.getLocalPort();//拼接一下 这下生成的是整个地址
        String payUrl = "http://" + address + "/pay?orderNo=" + orderNo;
        System.out.println(address);
        System.out.println(payUrl);
        try {
            QRCodeGenerator.generateQRCodeImage(payUrl, 350, 350, Constant.FILE_UPLOAD_DIR + orderNo + ".png");
        } catch (WriterException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //这个图片可以通过这个url访问到
        String pngAddress = "http://" + address + "/images/" + orderNo + ".png";
        return pngAddress;
    }
com/imooc/mall/util/QRCodeGenerator.java
package com.imooc.mall.util;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;

/**
 * 99.生成二维码工具
 */
public class QRCodeGenerator {
    public static void generateQRCodeImage(String text, int width, int height, String filePath) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);
        Path path = FileSystems.getDefault().getPath(filePath);
        MatrixToImageWriter.writeToPath(bitMatrix,"PNG",path);
    }

    public static void main(String[] args) {
        try {
            generateQRCodeImage("PANCHUNYAO", 350,350,"/Users/Pluminary/Desktop/idea_Space/mall_file_upload/QRTest.png");
        } catch (WriterException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
application.properties
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/mall_file_upload/
file.upload.ip=127.0.0.1
com/imooc/mall/service/impl/OrderServiceImpl.java
/**
 * 91.★★ 订单Service实现类 ★★
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    CartService cartService;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CartMapper cartMapper;
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    OrderItemMapper orderItemMapper;
    @Value("${file.upload.ip}")
    String ip;
............
}

后台订单列表接口

com/imooc/mall/controller/OrderAdminController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 100.订单后台管理Controller
 */
@RestController
public class OrderAdminController {
    @Autowired
    OrderService orderService;

    @GetMapping("admin/order/list")
    @ApiOperation("管理员订单列表")
    public ApiRestResponse listForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = orderService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
 //100.实现类
    @Override
    public PageInfo listForAdmin(Integer pageNum, Integer pageSize) {//想看到所有订单
        PageHelper.startPage(pageNum, pageSize);
        List<Order> orderList = orderMapper.selectAllForAdmin();
        List<OrderVO> orderVOList = orderListToOrderVOList(orderList);
        //新建一个pageinfo返回 方便前端查看
        PageInfo pageInfo = new PageInfo<>(orderList);
        pageInfo.setList(orderVOList);
        return pageInfo;
    }
com/imooc/mall/model/dao/OrderMapper.java
List<Order> selectAllForAdmin();
===========================================
mappers/OrderMapper.xml 
<select id="selectAllForAdmin" resultMap="BaseResultMap" parameterType="integer">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_order
    order by create_time desc
  </select>

支付

根据订单号生成支付二维码 → 扫码支付 → 访问支付URL, 完成支付
com/imooc/mall/controller/OrderController.java
 //101.支付接口
    @GetMapping("pay")
    @ApiOperation("支付接口")
    public ApiRestResponse pay(@RequestParam String orderNo){
        orderService.pay(orderNo);
        return ApiRestResponse.success();
    }
com/imooc/mall/service/impl/OrderServiceImpl.java
//101.支付
    @Override
    public void pay(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //支付前的判断 未付款才可以付款
        if (order.getOrderStatus() == Constant.OrderStatusEnum.NOT_PAID.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.PAID.getCode());
            order.setPayTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

完结订单

com/imooc/mall/controller/OrderAdminController.java
package com.imooc.mall.controller;

import com.github.pagehelper.PageInfo;
import com.imooc.mall.common.ApiRestResponse;
import com.imooc.mall.service.OrderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 100.订单后台管理Controller
 */
@RestController
public class OrderAdminController {
    @Autowired
    OrderService orderService;

    @GetMapping("admin/order/list")
    @ApiOperation("管理员订单列表")
    public ApiRestResponse listForAdmin(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
        PageInfo pageInfo = orderService.listForAdmin(pageNum, pageSize);
        return ApiRestResponse.success(pageInfo);
    }

    /**
     * 102.发货。订单状态流程:0用户已取消,10未付款,20已付款,30已发货,40交易完成
     */
    @GetMapping("admin/order/delivered")
    @ApiOperation("管理员发货")
    public ApiRestResponse delivered(@RequestParam String orderNo){
        orderService.deliver(orderNo);
        return ApiRestResponse.success();
    }

    /**
     * 103.完结订单。订单状态流程:0用户已取消,10未付款,20已付款,30已发货,40交易完成
     * 管理员和用户都可以调用
     */
    @GetMapping("order/finish")
    @ApiOperation("完结订单")
    public ApiRestResponse finish(@RequestParam String orderNo){
        orderService.finish(orderNo);
        return ApiRestResponse.success();
    }
}
com/imooc/mall/service/impl/OrderServiceImpl.java
 //102.管理订单实现类开发 发货
    @Override
    public void deliver(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //支付前的判断 判断已付款的
        if (order.getOrderStatus() == Constant.OrderStatusEnum.PAID.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.DELIVERED.getCode());
            order.setDeliveryTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }
    //103.完结订单
    @Override
    public void finish(String orderNo){
        //能查到订单就取消
        Order order = orderMapper.selectByOrderNo(orderNo);
        //查不到订单,报错
        if (order == null) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NO_ORDER);
        }
        //有可能管理员调用 有可能用户调用 如果是普通用户, 要去校验一下订单所属
        if (!userService.checkAdminRole(UserFilter.currentUser) && !order.getUserId().equals(UserFilter.currentUser.getId())) {
            throw new ImoocMallException(ImoocMallExceptionEnum.NOT_YOUR_ORDER);
        }
        //支付前的判断 判断已付款的 //发货后可以完结订单
        if (order.getOrderStatus() == Constant.OrderStatusEnum.DELIVERED.getCode()) {
            order.setOrderStatus(Constant.OrderStatusEnum.FINISHED.getCode());
            order.setEndTime(new Date());
            orderMapper.updateByPrimaryKeySelective(order);
        }else {
            throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_ORDER_STATUS);
        }
    }

全流程测试

  • Postman实操
  • 登录 → 浏览商品 → 加入购物车 → 下单
    • ​ → 取消订单
    • ​ → 扫码支付 → 发货 → 收获 → 订单完结
总结订单模块
  • 重难点

    • VO的封装、裁剪
    • 一个订单内包括多个商品
    • 订单状态流转
    • 二维码生成
  • 常见错误:把POJO返回到前端

上线前准备工作

阿里云部署

把每个 com/imooc/mall/model/request/… 类都加上**toString()**方法 方便调试
后台、前台、获取类的行为 的目录列表都是 GetMapping

前端准备工作

把static里的前端文件导入项目中
com/imooc/mall/config/ImoocMallWebMvcConfig.java
//104.ImoocMallWebMvcConfig.java 根据路由配置相关文件 以admin开头的文件会被路由到下面的Locations
package com.imooc.mall.config;

import com.imooc.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 42.配置地址映射 43去CategoryController.java 加一个 @ApiOperation("后台添加目录")
 * 44.新增一个目录的updateCategory的参数 [UpdateCategoryReq.java  ]
 */
@Configuration  //代表是一个配置
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
    public void addResourceHandles(ResourceHandlerRegistry registry){
        //104.根据路由配置相关文件 以admin开头的文件会被路由到下面的Locations
        registry.addResourceHandler("/admin/**").addResourceLocations("classpath:/static/admin");
        //65.增加一个registry  66.新增接口继续开发 ProductAdminController
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
//     把地址给到对应的目录下
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars");
    }
}

部署云服务器

  • 阿里云简介
  • 选择云服务器并购买
  • 环境配置
  • Spring Boot部署[包括maven打包 sql文件也要上传]

云服务器管理控制台 (aliyun.com)

免费试用三个月
账号: root
密码: panchunyao123!

复制服务器的公网ip:47.98.225.105
cmd本地电脑 → ssh root@47.98.225.105

在阿里云购买Linux服务器,配置宝塔环境,全图文,最最详细图解,保姆级教学_阿里云服务器怎么直接买宝塔-CSDN博客

【宝塔安装jdk1.8(yum安装)】

1、检索检索1.8的列表

yum list java-1.8*

2、安装1.8.0的所有文件

yum install java-1.8.0-openjdk* -y

3、使用命令检查是否安装成功

java -version

登录宝塔 → 绑定宝塔 → 下载软件
宝塔服务器[http://47.98.225.105:22003/]

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆ 宝塔BaoTa ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
========================面板账户登录信息=========================
 外网面板地址: http://47.98.225.105:22003/1259ebfa
 内网面板地址: http://172.24.91.49:22003/1259ebfa
 username: grltvl7d
 password: 46303f93


======================== mysql-5.7 ===========================
root密码成功修改为: Panchunyao123![mysql-8.0]

[root@iZbp1dssknxftmjczbtpndZ ~]# mysql -uroot -pPanchunyao123!
mysql> create database imooc_mall;
mysql> use imooc_mall;
Database changed
导入sql文件:imooc_mall_online.sql
从本机将其上传到服务器在cmd中运行:
scp /Users/Pluminary/Desktop/imooc_mall_online.sql root@47.98.225.105:22003:/root
输入服务器密码:panchunyao123!

回到服务器端输入:
mysql> use imooc_mall
Database changed
mysql> source /root/imooc_mall_online.sql
mysql> show tables;
+-----------------------+
| Tables_in_imooc_mall  |
+-----------------------+
| imooc_mall_cart       |
| imooc_mall_category   |
| imooc_mall_order      |
| imooc_mall_order_item |
| imooc_mall_product    |
| imooc_mall_user       |
+-----------------------+
#为数据库赋予权限!!!
mysql> grant all privileges on imooc_mall.* to 'root'@'127.0.0.1' identified by 'Panchunyao123!';
--------------------------------------------------------------
MySql8.0
CREATE USER 'root'@'47.98.225.105' IDENTIFIED BY 'Panchunyao123!';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'47.98.225.105' WITH GRANT OPTION;
--------------------------------------------------------------
mysql> quit
Bye
[root@iZbp1dssknxftmjczbtpndZ ~]# mysql -uroot -pPanchunyao123! -h127.0.0.1
去云服务器 安全组 访问规则 入方向 手动添加一个 目的:8081/8081 源:0.0.0.0/0
==============================================================

整理文件上传到服务器 先**Build → Rebuild Project **
打包 Maven → Lifecycle → clean[之前的内容删除 不容易出错] → package[打包操作]
spring打包报错:Java Runtime (class file version 55.0), class file versions up to 52.0_there was an error in the forked process org/testn-CSDN博客

[INFO] Building jar: C:\Users\Pluminary\Desktop\idea_Space\mall\target\mall-0.0.1-SNAPSHOT.jar
在本机cmd中运行把导好的包发送給服务器
C:\Users\Pluminary>scp C:\Users\Pluminary\Desktop\idea_Space\mall\target\mall-0.0.1-SNAPSHOT.jar root@c:/root
#rm -rf images/ 删除images文件夹
新建一个文件夹
[root@iZbp1dssknxftmjczbtpndZ ~]# pwd
[root@iZbp1dssknxftmjczbtpndZ ~]# mkdir image

图片上传到服务器中[图片尽量在没有根目录空格的文件夹下 比如Spring Boot 中间有空格]
C:\Users\Pluminary>scp C:/Users/Pluminary/Desktop/images/. root@47.98.225.105:/root/images/

在Xshell7中阿里云服务器
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
images  imooc_mall_online.sql  install.sh  mall-0.0.1-SNAPSHOT.jar

每一次部署之前要把当前的java停止掉
[root@iZbp1dssknxftmjczbtpndZ images]# lsof -i:8081
[root@iZbp1dssknxftmjczbtpndZ images]# kill -9 12345 //按照PID杀死端口号
-bash: kill: (12345) - No such process

部署到云服务器 [/root/null 2>&1 &]将日志输出到哪里 这里是丢弃的意思
[root@iZbp1dssknxftmjczbtpndZ ~]# nohup java -jar -Dserver.port=8081 -Dspring.profiles.active=prod /root/mall-0.0.1-SNAPSHOT.jar > /root/null 2>&1 &
[1] 18656
如果没有别名就一个本来的application.properties
[root@iZbp1dssknxftmjczbtpndZ ~]# nohup java -jar -Dserver.port=8081 /root/mall-0.0.1-SNAPSHOT.jar > /root/null 2>&1 &


 #代表当前程序的进程号
[root@iZbp1dssknxftmjczbtpndZ ~]# lsof -i:8081   //查询端口号
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    18656 root   19u  IPv6 195648      0t0  TCP *:tproxy (LISTEN)

============================================================================

若打不开请检查网页报错 寻找传入的application.prod.properties的 
spring.datasource.url地址和spring.datasource.username && password数据库用户和密码

查看服务器的端口: mysql> show global variables like 'port';

阿里云控制台:
https://ecs.console.aliyun.com/server/i-bp1dssknxftmjczbtpnd/detail?regionId=cn-hangzhou

如何查看linux服务器安装了tomcat:
rpm -qa|grep tomcat

宝塔安装目录: $cd /www/server/.....
[root@iZbp1dssknxftmjczbtpndZ data]# cd /www/server/tomcat/bin
#找到文件后启动tomcat
[root@iZbp1dssknxftmjczbtpndZ bin]# ./startup.sh
#看进程,如果返回一大串东西,说明tomcat启动成功。
[root@iZbp1dssknxftmjczbtpndZ ~]# ps -ef|grep tomcat

运行SpringBoot: 
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
Desktop  images  imooc_mall_online.sql  install.sh  logs  mall-0.0.1-SNAPSHOT.jar  null
[root@iZbp1dssknxftmjczbtpndZ ~]# java -jar mall-0.0.1-SNAPSHOT.jar 

===============================================
当把SpringBoot项目的jar包部署到linux服务器中,启动SpringBoot项目,却无法正常访问,这是怎么回事呢?
主要的原因是端口号被Linux的firewall防火墙拦截掉了
解决办法:
1、查询已开启的端口列表:firewall-cmd --list-ports 
不出意外的话,是啥也没有,这也表示,所有端口都未放行,当然springboot项目的80端口也被拦截掉了
2、firewall-cmd --zone=public --add-port=1-12345/tcp --permanent
将端口1~12345全都开启
3、重启防火墙
service firewalld restart
4、重新运行springboot项目
5、成功访问
===============================================

server.port=8083
spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://47.98.225.105/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=Panchunyao123!
mybatis.mapper-locations=classpath:mappers/*.xml
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
file.upload.dir=/root/images/
file.upload.ip=47.98.225.105
icode=ABCDE

SpringBoot上传图片,图片不能及时显示问题_springboot项目上传照片到uoload 不会实时更新-CSDN博客
Springboot 上传图片到项目路径下不能访问,需要重启_springboot能上传图片到win11中但无法访问图片-CSDN博客
关于springboot项目图片上传到本地,必须重启之后才能访问的解决方案_springboot上传图片需要重启-CSDN博客
关于IDEA2022开启热部署没有compiler.automake.allow.when.app.running的解决方案-CSDN博客
Linux部署SpringBoot项目无法访问问题_linux springboot项目启动外面不能访问-CSDN博客
linux服务器部署SpringBoot项目并查看项目运行日志_linux 怎么实时查看springboot 日志-CSDN博客
线上发布报数据库没有连接成功,麻烦帮忙看…-慕课网 (imooc.com)
linux安装mysql 8 数据库(保姆级)_linux安装mysql8-CSDN博客
慕慕生鲜详细步骤全部打通(从无到上线)-CSDN博客

mysql8.0密码=> root & Panchunyao123!

阅读全文

SSM开发社交网站[Freemarker+Spring+SpringMVC+Mybatis(Plus)构成SSM+Bootstrap+Art-Template+Kaptcha+SpringTask+wangEditor]

2024/1/18

慕课书评网学习内容

SSM => Spring / Spring MVC / MyBatis

  • 讲解Spring/Spring MVC/MyBatis(SSM)整合配置过程
  • 讲解MyBatis-Plus敏捷开发插件的用法
  • 开发Java Web应用”慕课书评网”

主要知识点

  • SSM整合配置
  • MyBatis-Plus配置与应用
  • Kaptcha验证码组件使用
  • 富文本编辑器wangEditor
  • Spring Task任务调度
  • 基于阿里云实现短信验证
  • 基于腾讯云的滑块验证码实现前置的人机登录检查

工程结构与开发规约 [复习知识点]

工程结构
mgallery - eclipse工程项目
 /src - java源代码目录
 /WebContent - Web资源目录
 /css - css文件目录
 /js - js文件目录
 /image - 图片资源目录
 /upload - 上传文件目录
 /WEB-INF   //jsp数据来自controller 不允许在web中直接访问 要从控制器跳转
   /jsp - jsp页面目录
   /lib - jar文件目录
   /classes - 编译后的class目录
   /web.xml web描述符文件
包结构
com.imooc.mgallery //逆命名法
    /controller - 存放Servlet控制器类 //承上启下接收参数 调用逻辑 返回处理结果
    /service - 存放处理逻辑类model[伪数据库] //完成业务逻辑 service与dao进行传递调用
    /dao - Data Access Object 数据访问对象类 数据读写的java类 数据来自xml文件
    /entity - 存放实体类 JavaBean java中的简单对象
    /utils - 通用工具类 底层通用的工具类或方法

什么是整合

  • 通过Spring IoC容器管理第三方框架对象, 让多框架形成整体
  • SSM => Spring / Spring MVC / MyBatis是业内最主流的框架搭配
    • Spring MVC提供了控制器 基于Spring的Web交互的能力
    • Spring是对应用程序的对象进行创建和管理
    • MyBatis是完成了与底层数据库实现增删改查的操作
  • SSM配置与使用是所有Java工程师必须掌握的技能

SSM整合三阶段

  • Spring与Spring MVC环境配置
  • Spring与MyBatis的整合配置
  • 整合其他组件: 声明式事务/日志/任务调度/…

Spring与Spring MVC环境配置

  • 依赖Spring-WebMVC
  • 配置DispatcherServlet
  • 启用Spring MVC注解模式
  • 配置请求与相应字符集
  • 配置FreeMarker模板引擎
  • 配置JSON序列化组件

编写pom.xml 引入导入的依赖jar包
IntelliJ IDEA 2021 自动下载pom文件中的依赖_pom中的自定义插件下载-CSDN博客
idea编译Java项目的部分java文件并且生成class文件_idea将java文件编译成class-CSDN博客

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
设置resource和source
方法:点击文件右键->make direction as

需要运行和引用的java文件设置为source文件,将配置文件(xml)等设置为resource文件
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>imooc-reader</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
<!--1.Maven依赖Spring-webmvc 第二步web.xml-->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--    Freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
<!--   Jackson依赖包  -->
        <dependency>
            <artifactId>jackson-core</artifactId>
            <groupId>com.fasterxml.jackson.core</groupId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>
</project>
WEB-INF/web.xml    init-param变量 这个是放在servlet里面的,只有此servlet可以访问
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
<!--    2.配置DispatcherServlet 第三步applicationContext.xml-->
 <!--    DispatcherServlet是Spring MVC是最核心的对象
         DispatcherServlet用于拦截http请求        -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <!--自动加载applicationContext-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext*.xml</param-value>
        </init-param>
<!--        启动时初始化 servlet-->
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping> <!--与上面保持一致-->
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
<!-- 4.解决中文乱码问题  只会对Post请求生效 Get只需要修改tomcat的server.xml(8.0以上是自动的) 72行增加URIEncoding="UTF-8" 第五步application-->
    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param> <!--初始化参数-->
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/task
            http://www.springframework.org/schema/task/spring-task.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 3.Spring框架启用SpringMVC注解模式 第四步web.xml-->
    <context:component-scan base-package="com.imooc"/>
<!-- 真正开启SpringMVC注解 -->
<!-- https://blog.csdn.net/u011066470/article/details/112438252 -->
<!-- <mvc:annotation-driven> Spring MVC用来提供Controller请求转发,json自动转换等功能。,默认会帮我们注册默认处理请求,参数和返回值的类 -->
    <mvc:annotation-driven>
        <mvc:message-converters>
<!-- 解决相应里的中文输出 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=utf-8</value>
                        <!-- 6.JSON(Jackson)序列化输出配置   -->
                        <value>application/json;charset=utf-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
<!-- 诸如css/图片/js静态资源排除在外 使得SpringMVC对url处理效率加大   -->
    <mvc:default-servlet-handler/>
<!-- 5.配置Freemarker模板引擎 脚本存放地址 Bean定义可以具有零个或多个属性。属性元素对应于bean类公开的JavaBean setter方法。 Spring支持原语,对相同或相关工厂中的其他bean的引用,列表,映射和属性。 -->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/ftl"/>
<!--        对于本身的参数配置  -->
        <property name="freemarkerSettings">
            <props>
<!--        默认的编码类型 UTF-8 freemarker读取指定ftl文件时采用此字符集-->
                <prop key="defaultEncoding">UTF-8</prop>
            </props>
        </property>
    </bean>
<!--    使用何种模板引擎 创建的后缀名 -->
    <bean id="ViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="contentType" value="text/html;charset=utf-8"/>
        <property name="suffix" value=".ftl"/>
    </bean>
</beans>
WEB-INF/ftl/test.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
测试页面!!
</body>
</html>
com/imooc/controller/TestController.java
package com.imooc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;

@Controller
public class TestController {
    @GetMapping("/test/t1")
    public ModelAndView test1(){
        return new ModelAndView("/test");
    }
    @GetMapping("test/t2")
    @ResponseBody
    public Map test2(){
        Map result = new HashMap();
        result.put("test","测试文本");
        return result;
    }
}

IDEA Web项目out/classes目录没有编译的class类文件 - IDEA环境下创建Maven WebApp_out中没有class文件夹-CSDN博客

Spring与MyBatis整合配置

MyBatisUtils封装初始化代码 如果自己new了对象就不会享受spring的福利待遇

  • 依赖mybatis-spring及驱动
  • 配置数据源与连接池
  • 配置SqlSessionFactory
  • 配置Mapper扫描器
  • 创建mybatis-config.xml
applicationContext.xml
<!-- 5.配置Freemarker模板引擎 脚本存放地址 第六步见pom.xml mybatis依赖 -->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/ftl"/>
<!--        对于本身的参数配置  -->
        <property name="freemarkerSettings">
            <props>
<!--        默认的编码类型 UTF-8 freemarker读取指定ftl文件时采用此字符集-->
                <prop key="defaultEncoding">UTF-8</prop>
            </props>
        </property>
    </bean>
<!--    使用何种模板引擎 创建的后缀名   -->
    <bean id="ViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="contentType" value="text/html;charset=utf-8"/>
        <property name="suffix" value=".ftl"/>
    </bean>
<!--    7. MyBatis与Spring的整合配置   -->
      <!--②.配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/imooc_reader?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="5"/>
        <property name="maxActive" value="20"/>
    </bean>
<!--8. SqlSessionFactoryBean用于根据配置信息创建SqlSessionFactory,不再需要我们自己编码创建-->
    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--    存储sql语句的mapper.xml文件 存储在哪个目录中 去创建mapper接口-->
        <property name="mapperLocations" value="classpath:mappers/*.xml"/>
<!-- 10.  MyBatis配置文件地址 去创建一个xml  -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
<!--  9. 配置Mapper扫描器 下一步9TestMapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--  这样初始化mybatis时 会对所有mapper接口进行扫描 -->
    <property name="basePackage" value="com.imooc.reader.mapper"/>
</bean>
pom.xml
<!--  6. Mybatis整合步骤:①引入依赖 第七步看applicationContext.xml-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>
<!--        MyBatis与Spring整合组件 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>
com/imooc/reader/mapper/TestMapper.java
package com.imooc.reader.mapper;
//接口作用Mybatis通过接口自动生成实现类
public interface TestMapper {
// 系统中接口很多不是所有的都对应mapper 9.application.xml
    public void insert();
}
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
<!--        驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

整合其他组件

  • 配置logback日志输出
  • 声明式事务配置
  • 整合JUnit单元测试
pom.xml
<!--   11. 单元测试依赖 mappers加入test.xml mapper扫描接口后生成对应实现类 12test.xml-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
<!--  15导入依赖 scope打包时不会放进去 tomcat自带 但是测试时不用tomcat所以要加-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
<!--   16加入日志依赖 logback日志组件 17创建logback.xml-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
mappers/test.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.TestMapper">
<!--   12. 与接口对应的完整类路径  ↓用于对应数据新增 对应TestMapper.java接口  13TestService -->
    <insert id="insert">
        insert into test(content) values('测试内容')
    </insert>
</mapper>
com/imooc/reader/service/TestService.java
package com.imooc.reader.service;

import com.imooc.reader.mapper.TestMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class TestService {
// 13.运行时注入生成的对象 自动生成Test 选中上面的类 Code generate 14TestServiceTest.java
    @Resource
    private TestMapper testMapper;
//      @Transactional 要么全部完成 要么什么也不做
    @Transactional
    public void batchImport(){
        for (int i = 0; i < 5; i++) {
//            if (i==3) {
//                throw new RuntimeException("预期外异常");
//            }
            testMapper.insert();
        }
    }
}
com/imooc/reader/service/TestServiceTest.java
package com.imooc.reader.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import static org.junit.Assert.*;

//14. 运行时自动初始IoC容器 + 说明配置文件在什么地方完成初始化 15依赖javaservlet pom.xml
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TestServiceTest {
    @Resource
    private TestService testService;

    @Test
    public void batchImport() {
        testService.batchImport();
        System.out.println("批量导入成功");
    }
}
logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--           17.       小时分钟秒   日志级别  线程名   说明何类何方法  对应日志消息+换行-->
<!--            18转至声明式事务 applicationContext上面的文件头加些配置信息 再去下面写-->
            <pattern>%d{HH:mm:ss} %-5level [%thread] %logger{30} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
<!--    输出级别   -->
    <root level="debug">
        <appender-ref ref="console"/>
    </root>
</configuration>
applicationContext.xml
<!--    18.声明式事务配置 控制事务的打开 提交 回滚 ↓-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    与之对应的注解模式! 再去TestService.java添加  @Transactional -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
@RunWith作用
@RunWith 就是一个运行器
@RunWith(JUnit4.class) 就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境
@RunWith(Suite.class) 的话就是一套测试集合,
@ContextConfiguration Spring整合JUnit4测试时,使用注解引入多个配置文件
单个文件
@ContextConfiguration(Locations=“classpath:applicationContext.xml”)
@ContextConfiguration(classes = SimpleConfiguration.class)

多个文件时,可用{}
@ContextConfiguration(locations = { “classpath:spring1.xml”, “classpath:spring2.xml” })

MyBatis-Plus

MyBatis-Plus (baomidou.com)

  • MyBatis-Plus(简称MP)是一个MyBatis的增强工具
  • 自动实现Mapper CRUD操作, 极致提高数据库开发效率
  • MP在MyBatis的基础上只做增强不做改变

MyBatis-Plus整合三部曲

  • pom引入mybatis-plus依赖
  • Spring XML更改配置SqlSessionFactory实现类
  • mybatis-config.xml增加MP分页插件

MyBatis-Plus开发三部曲

  • 创建实体类, @TableName/ @Tableld/ @TableField实现映射

  • 创建Mapper接口继承BaseMapper, 创建Mapper XML

  • 开发时注入Mapper对象, 通过内置API实现CRUD操作

MyBatis-Plus核心注解

  • @TableName - 将实体类与表名映射
  • @Tableld - 说明对应属性是表的主键
  • @TableField - 设置属性与列名的对应关系

BaseMapper接口核心API

方法名 用途
insert(entity) 数据新增,自动生成insert sql, 根据@Tableld决定注解生成方式
updateById(entity) 根据主键更新对应对象, 自动生成update sql
deleteById(id) 根据主键删除数据, 自动更新delete sql
selectById(id) 按主键查询对应的实体对象
selectList(queryWrapper) 根据查询生成器(QueryWrapper)的条件自动生成sql查询返回List集合
selectPage(page,queryWrapper) 分页查询方法, 自动生成分页limit子句, 返回IPage分页对象
pom.xml
<!--   19 整合Mybatis-plus依赖  修改application中65行   -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
applicationContext.xml  【替换之前的sessionFactory】
<!--  19. 原生Mybatis与Spring整合  自动实现接口的增删改查  20增加plus底层的分页查询插件 mybatis-config.xml-->
<!--    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">-->
    <bean id="sessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--    存储sql语句的mapper.xml文件 存储在哪个目录中 去创建mapper接口-->
        <property name="mapperLocations" value="classpath:mappers/*.xml"/>
<!-- 10.  MyBatis配置文件地址 去创建一个xml  第十一步pom加单元测试依赖-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
<!--  9. 配置Mapper扫描器  -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--  这样初始化mybatis时 会对所有mapper接口进行扫描 -->
    <property name="basePackage" value="com.imooc.reader.mapper"/>
</bean>
<!--    18.声明式事务配置 控制事务的打开 提交 回滚 ↓-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    与之对应的注解模式! 再去TestService.java添加  @Transactional -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
<!--        驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <plugins>
<!--  20.配置Mybatis-Plus分页查询     21创entity/Test.java-->
        <plugin interceptor="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor"></plugin>
    </plugins>
</configuration>
com/imooc/reader/entity/Test.java
// 21 Mybatis-plus运用   22mapper中TestMapper
@TableName("test") //说明实体对应哪一张表
public class Test {
    @TableId(type = IdType.AUTO) //数据自增
    @TableField("id") //说明属性对应哪个字段
    private Integer id;
    //如果字段名与属性名相同或者符合驼峰命名转换规则 则TableField可以省略
    @TableField("content")
    private String content;
} Getter + Setter
com/imooc/reader/mapper/TestMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Test;
//22.为了和BaseMapper里的insert()区别开 这个改名成insertSample
//接口作用Mybatis通过接口自动生成实现类 23.MyBatisPlusTest.java
public interface TestMapper extends BaseMapper<Test> {
// 系统中接口很多不是所有的都对应mapper 9.application.xml
    public void insertSample();
}
com/imooc/reader/MyBatisPlusTest.java
package com.imooc.reader;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Test;
import org.junit.runner.RunWith;
import com.imooc.reader.mapper.TestMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.List;

//23.利用插件调用增删改查  24以Bootstrap开发前端index.ftl
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class MyBatisPlusTest {
    @Resource
    private TestMapper testMapper;

    //增
    @org.junit.Test //直接进行调用 与注解重名 故此加包名
    public void testInsert() {
        Test test = new Test();
        test.setContent("MyBatis Plus测试");
        //在接口中 com/imooc/reader/mapper/TestMapper.java
        testMapper.insert(test);
    }

    //改
    @org.junit.Test
    public void testUpdate() {
        Test test = testMapper.selectById(30);//按id号查询
        test.setContent("MyBatis Plus测试1");
        testMapper.updateById(test);
    }

    //删
    @org.junit.Test
    public void testDelete() {
        testMapper.deleteById(30);
    }

    //查
    @org.junit.Test
    public void testSelect() {
        //传入特殊包装对象
        QueryWrapper<Test> queryWrapper = new QueryWrapper<Test>();
//        queryWrapper.eq("id", 31); //eq是等值比较 若写多行语句则sql语言用and连接
        queryWrapper.gt("id", 5);//选择范围 查询大于5的数据
        List<Test> list = testMapper.selectList(queryWrapper);//返回获取7号数据的集合
        System.out.println(list.get(0));
    }
}

基于SSM开发慕课书评网

导入训练素材的imooc-reader.sql

Bootstrap入门介绍 [优秀的前端UI框架]
  • Bootstrap是全球最受欢迎的前端组件库, 由推特(TWitter)开源
  • Bootstrap用于开发响应式布局、移动设备优先的WEB项目
  • Bootstrap提供完整的HTML、CSS和JS开发工具集

Bootstrap中文网 (bootcss.com)

显示图书类别 [全部 | 前端 | 后端 | 测试 | 产品]

index.ftl [只写入顺序关键信息 在这里插入素材中的index.html并更名]
<#--在这块进行动态数据编写 24.创建entity/Category分类实体-->
        <div class="col-8 mt-2">
            <span data-category="-1" style="cursor: pointer" class="highlight  font-weight-bold category">全部</span>
            |
<#--     31.对页面进行动态的数据导入       -->
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
<#--                取消最后一个的竖线-->
                <#if category_has_next>|</#if>
            </#list>
        </div>
com/imooc/reader/entity/Category.java
// 24.图书分类实体 25mapper创建一个CategoryMapper
public class Category {
    @TableId(type = IdType.AUTO)
    private Long categoryId;
// 已自动配置好了驼峰命名法 可以忽略
// @TableField("category_name")
    private String categoryName;
}Getter + Setter + 重写toString
com/imooc/reader/mapper/CategoryMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Category;
//25.BaseMapper自动提供了增删改查的功能 泛型指向Category
// 图书分类Mapper接口 26在mappers中创建一个category.xml
public interface CategoryMapper extends BaseMapper<Category> {

}
mappers/category.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.CategoryMapper">
<!-- 26.如何使用呢? 27创建一个service/CategoryService接口-->
</mapper>
com/imooc/reader/service/CategoryService.java
package com.imooc.reader.service;
//27.有了service的接口 28就要有实现实现类存放包CategoryServiceImpl

import com.imooc.reader.entity.Category;
import java.util.List;

public interface CategoryService {
    public List<Category> selectAll();
}
com/imooc/reader/service/impl/CategoryServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Category;
import com.imooc.reader.mapper.CategoryMapper;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

//28.向接口编程的规则,注入写好的CategoryMapper接口 设置Transactional事务传播
//默认所有方法是不使用事务的 查询较多的方法不使用事务 写入方法较多下写入事务
//29生成测试类 code-generate-Test  com/imooc/reader/service/impl/CategoryServiceImplTest.java
@Service("categoryService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class CategoryServiceImpl implements CategoryService {
    @Resource
    private CategoryMapper categoryMapper;
    /**
     * 查询所有图书分类
     * @return 图书分类List
     */
    @Override
    public List<Category> selectAll() {
        //查询列表 返回多个数据 插入条件构造器[查询所有]
        List<Category> list = categoryMapper.selectList(new QueryWrapper<Category>());
        return list;
    }
}
com/imooc/reader/service/impl/CategoryServiceImplTest.java[Test]
package com.imooc.reader.service.impl;

import com.imooc.reader.entity.Category;
import com.imooc.reader.mapper.CategoryMapper;
import com.imooc.reader.service.CategoryService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import java.util.List;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class CategoryServiceImplTest {
    @Resource
    //29.保证属性和刚才的 @Service("categoryService")一致 重写Category的tostring方法重新运行selectAll
    //30进入controller开发环节 新建url与方法绑定的BookController.java
    private CategoryService categoryService;
    @Test
    public void selectAll() {
        List<Category> list = categoryService.selectAll();
        System.out.println(list);
    }
}
com/imooc/reader/controller/BookController.java
package com.imooc.reader.controller;

import com.imooc.reader.entity.Category;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import java.util.List;

//30.显示之前写好的index.ftl
@Controller
public class BookController {
    //传入模板数据 分类信息
    @Resource
    private CategoryService categoryService;
    //showIndex与url绑定  31继续向前推进因为index.ftl所有数据都是静态写死的
    //31要对每一个分类进行读取 List标签<#list>读取 转至index.ftl 64行
    /**
     * 显示首页
     * @return
     */
    @GetMapping("/")
    public ModelAndView showIndex(){
        ModelAndView mav = new ModelAndView("index");
        List<Category> categoryList = categoryService.selectAll();//拿到列表
        //结果放入其中 属性名,值
        mav.addObject("categoryList",categoryList);
        return mav;
    }
}

----------------------------------------------------------
index.ftl
<#-- 31.对页面进行动态的数据导入 32创建全新实体类Book -->
            <#list categoryList as category>
                <a style="cursor: pointer" data-category="${category.categoryId}" class="text-black-50 font-weight-bold category">${category.categoryName}</a>
<#--   取消最后一个的竖线 -->
                <#if category_has_next>|</#if>
            </#list>
        </div>

实现图书分页查询

com/imooc/reader/entity/Book.java
// 32.创建完Book实体类  33去创建全新的BookMapper接口[mapper中]
@TableName("book")
public class Book {
    @TableId(type = IdType.AUTO)
    private Long bookId;
    private String bookName;
    private String subTitle;
    private String author;
    private String cover;
    private String description;
    private Long categoryId;
    private Float evaluationScore;
    private Integer evaluationQuantity;
}Getter + Setter
com/imooc/reader/mapper/BookMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Book;
//33.创建完Book接口  34与之对应的创建book.xml文件
public interface BookMapper extends BaseMapper<Book> {
}
mappers/book.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.BookMapper">
<!--33.完成底层与数据交互代码 34完成分页处理 是在Service中完成的创建BookService完成分页对象查询-->
</mapper>
com/imooc/reader/service/BookService.java
package com.imooc.reader.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
//图书服务
public interface BookService {
    //34.泛型<Book> 说明查询出来的每一行数据都是一个Book对象
    // 瞄准BookService快速生成实现类Alt+Enter => Implement interface
    //35编写com.imooc.reader.service.impl.BookServiceImpl
    public IPage<Book> paging(Integer page, Integer rows);
}
com/imooc/reader/service/impl/BookServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imooc.reader.entity.Book;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

//35. 实现分类代码编写  36生成测试用例 Code generate Test => BookServiceImplTest
@Service("bookService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class BookServiceImpl implements BookService {
    @Resource
    private BookMapper bookMapper;
    /**
     * 分页查询图书
     * @param page 页号
     * @param rows 每页记录数
     * @return 分页对象
     */
    public IPage<Book> paging(Integer page, Integer rows) {
        Page<Book> p = new Page<Book>(page,rows);
        QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
        IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);//传入两个参数: page对象哪一页数据  每页记录数[条件构造器]
        return pageObject;
    }
}
com/imooc/reader/service/impl/BookServiceImplTest.java[Test]
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
import com.imooc.reader.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import java.util.List;

import static org.junit.Assert.*;
//36.测试分页的代码编写  37去BookController新加一个方法
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class BookServiceImplTest {
    @Resource
    private BookService bookService;
    @Test
    public void paging() {
        //查询每页十条
        IPage<Book> pageObject = bookService.paging(1,10);
        //获取当前页数据
        List<Book> records = pageObject.getRecords();
        for (Book b:records){
            System.out.println(b.getBookId() + ":" + b.getBookName());
        }
        System.out.println("总页数:" + pageObject.getPages());
        System.out.println("总记录数:" + pageObject.getTotal());
    }
}

Ajax动态加载图书信息

com/imooc/reader/controller/BookController.java
//37.编写一个page方法 上面加个@Resource 绑定一个Ajax @GetMapping
    //http://localhost/books 非常长的JSON序列化结果  38index.ftl 83行
    /**  
     * 分页查询图书列表
     * @param p 页号
     * @return 分页对象
     */
package com.imooc.controller;

import com.imooc.reader.entity.Book;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;

@Controller
public class BookController {
    @Resource
    private BookService bookService;
    
    @GetMapping("/books")
    @ResponseBody //具体数据来自客户端传入
    public IPage<Book> selectBook(Long categoryId, String order, Integer p) {
        if (p == null) {
            p = 1;
        }
        IPage<Book> pageObject = bookService.paging(categoryId, order, p, 10);
        return pageObject;
    }
}

img标签src引用网络图片,响应403的解决方法_img src引用其他网站图片-CSDN博客

index.ftl
  <#--    38.编写Json序列化格式并追加到当前网页 上方以导入Art-Template模板引擎 39将<div id="bookList">下面的代码裁切放到上面 http://localhost/ -->
    <script>
        $(function () {
            $.ajax({
                url: "/books",
                data : {p:1},
                type : "get",
                dataType : "json",
                success: function(json){
                    console.info(json);
                    var list = json.records;//获取当前分页数据
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        //动态将数据组合成html
                        var html = "<li>" + book.bookName + "</li>";
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                }
            })
        })
    </script>

Art - Template 腾讯JS模板引擎

art-template (aui.github.io)

整段HTML模板化 静态文本不变化 动态的数据用响应表达式进行提取

两组大括号进行数据提取

index.ftl
<meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="./resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="./resources/raty/lib/jquery.raty.css">
    <script src="./resources/jquery.3.3.1.min.js"></script>
    <script src="./resources/bootstrap/bootstrap.min.js"></script>
    <script src="./resources/art-template.js"></script>
    <script src="./resources/raty/lib/jquery.raty.js"></script>
-------------------------------------------------------------------------------------
<#--39.type="text/html"说明当前script块中包含的内容是一段一段的html浏览器不会作为javascript进行解析-->
<#-- ★ ★ ★ id是模板名字 两组大括号进行提取数据 ★ ★ ★ 使用模板引擎简化产生html过程-->
<#--    40去修改下方script代码导入的形式-->
    <script type="text/html" id="tpl">
        <a href="/book/{{bookId}}" style="color: inherit">
            <div class="row mt-2 book">
                <div class="col-4 mb-2 pr-2">
                    <img class="img-fluid" src="{{cover}}">
                </div>
                <div class="col-8  mb-2 pl-0">
                    <h5 class="text-truncate">{{bookName}}</h5>

                    <div class="mb-2 bg-light small  p-2 w-100 text-truncate">{{author}}</div>


                    <div class="mb-2 w-100">{{subTitle}}</div>

                    <p>
                        <span class="stars" data-score="{{evaluationScore}}" title="gorgeous"></span>
<#--                        <img alt="1" 加入了星型组件免去这些操作 -->
<#--                             src="./resources/raty/lib/images/star-on.png"-->
<#--                             title="gorgeous">&nbsp;<img alt="2"-->
<#--                                                         src="./resources/raty/lib/images/star-on.png"-->
<#--                                                         title="gorgeous">&nbsp;<img-->
<#--                                alt="3" src="./resources/raty/lib/images/star-on.png" title="gorgeous">&nbsp;<img-->
<#--                                alt="4" src="./resources/raty/lib/images/star-on.png" title="gorgeous">&nbsp;<img-->
<#--                                alt="5" src="./resources/raty/lib/images/star-on.png" title="gorgeous"><input-->
<#--                                name="score" type="hidden" value="{{evaluationScore}}" readonly=""></span>-->
                        <span class="mt-2 ml-2">{{evaluationScore}}</span>
                        <span class="mt-2 ml-2">{{evaluationQuantity}}人已评</span>
                    </p>
                </div>
            </div>
        </a>
    </script>

<#--    38.编写Json序列化格式并追加到当前网页 上方以导入Art-Template[js]模板引擎 39将<div id="bookList">下面的代码裁切放到上面-->
        <script>
<#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件-->
            $.fn.raty.defaults.path = "./resources/raty/lib/images"
            $(function () {
                $.ajax({
                    url: "/books",
                    data : {p:1},
                    type : "get",
                    dataType : "json",
                    success: function(json){
                        console.info(json);
                        var list = json.records;//获取当前分页数据
                        for (var i = 0; i < list.length; i++) {
                            var book = json.records[i];
                            // 动态将数据组合成html
                            // var html = "<li>" + book.bookName + "</li>";
                            // 40.替换导入数据形式 (传入模板id,传入数据) 将数据结合tpl模板,生成html
                            // 41当评分降低的时候顺带着星星的亮度降低 raty星型评分组件 上面已准备好raty css js引入
                            // 41在上面几行引入星型评分的script方法
                            var html = template("tpl", book);
                            console.info(html);
                            //jquery的id选择器选中div对象 追加
                            $("#bookList").append(html);
                        }
                        //42.显示星型评价组件 只读并不能修改
                        $(".stars").raty({readonly:true});
                    }
                })
            })
    </script>

实现图书列表分页查询

index.ftl
// 43.用于绑定加载更多按钮单击事件 44下面的设置页面的逻辑及处理
        $(function () {
            $("#btnMore").click(function (){
                loadMore();
            })
        })
<script>
<#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件(被注释)-->
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        // 45.对两次Ajax的代码进行重构与梳理(定义一个loadMore()方法将下面的裁切进来底下写进递归)
        // isReset参数设置true,代表从第一页开始查询,否则按nextPage查询后续页
        // 对下面那个data: {p: 1}, 进行重构 ↓↓↓↓ isReset 下面代码已经被注释但未完全删除
        // loadMore()加载更多数据
        function loadMore(isReset){
            if (isReset == true){
                $("#nextPage").val(1);
            }
            var nextPage = $("#nextPage").val();
            $.ajax({
                url: "/books",
                //nextPage为2可以加载第二页
                data: {p: nextPage},
                type: "get",
                dataType: "json",
                success: function (json) {
                    console.info(json);
                    var list = json.records;//获取当前分页数据
                    for (var i = 0; i < list.length; i++) {
                        var book = json.records[i];
                        var html = template("tpl", book);
                        console.info(html);
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readonly: true});
                    // 44.如果当前页小于总页数 下面利用val设置隐藏域的值
                    // 可能将按照字符串处理 结果是31而不是4 要强制转换
                    // 若有后续数据的话 最后几行的divNoMore需要隐藏起来
                    // 45对两次的Ajax代码进行重构与梳理 上面找到第一个script
                    if (json.current < json.pages){
                        $("#nextPage").val(parseInt(json.current + 1));
                        $("#btnMore").show();
                        $("#divNoMore").hide();
                    }else {
                        $("#btnMore").hide();
                        $("#divNoMore").show();
                    }
                }
            })
        }
     
        $(function () {
            loadMore(true);
        })

        // 43.用于绑定加载更多按钮单击事件 44上面的设置页面的逻辑及处理
        $(function () {
            $("#btnMore").click(function (){
                loadMore();
            })
        })
    </script>

实现图书多条件动态查询

index.ftl
// 43.用于绑定加载更多按钮单击事件 44上面的设置页面的逻辑及处理
        $(function(){
            $("#btnMore").click(function(){
                loadMore();
            })
            //46 增加点击的显示控件
            $(".category").click(function () {
                $(".category").removeClass("highlight"); //移除高亮
                $(".category").addClass("text-black-50");//增添灰色
                $(this).addClass("highlight");//增加高亮
                //49添加数量与热度 ↓点击的超链接 在下面也要添加order
                var categoryId = $(this).data("category"); //214行定义了data-category
                $("#categoryId").val(categoryId);
                loadMore(true); //每次点完要重新查询
            })//在排序处显示控件 47点击 全部|前端|后端 产生数据联动BookService上

            $(".order").click(function(){
                $(".order").removeClass("highlight"); //移除高亮
                $(".order").addClass("text-black-50");//增添灰色
                $(this).addClass("highlight");//增加高亮
                //~49.提取设置到隐藏域中 发送请求到Ajax服务器上最上面 50var categoryId
                var order = $(this).data("order");
                $("#order").val(order); //点击不同隐藏域时为其赋值
                loadMore(true);
            })
        })
com/imooc/reader/service/BookService.java
package com.imooc.reader.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
//图书服务
public interface BookService {
    //34.泛型<Book> 说明查询出来的每一行数据都是一个Book对象
    // 瞄准BookService快速生成实现类Alt+Enter => Implement interface
    //35编写com.imooc.reader.service.impl.BookServiceImpl

    /**
     * 分页查询图书
     * @param categoryId 分类编号
     * @param order 排序方式
     * @param page 页号
     * @param rows 每页记录数
     * @return 分页对象
     */
//    47增加两个变量且在BookServiceImpl中也对应增加上
    public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows);
}
package com.imooc.reader.service.impl.BookServiceImpl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imooc.reader.entity.Book;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.service.BookService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

//35. 实现分类代码编写  36生成测试用例 Code generate Test => BookServiceImplTest
@Service("bookService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class BookServiceImpl implements BookService {
    @Resource
    private BookMapper bookMapper;

    /**
     * 分页查询图书
     *
     * @param categoryId 分类编号
     * @param order      排序方式
     * @param page       页号
     * @param rows       每页记录数
     * @return 分页对象
     */
    public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows) {
        Page<Book> p = new Page<Book>(page, rows);
        QueryWrapper<Book> queryWrapper = new QueryWrapper<Book>();
        //47.编写匹配条件 48去BookController更改调用参数
        if (categoryId != null && categoryId != -1) {//代表传入了有效分类编号
            queryWrapper.eq("category_id", categoryId);//查询的where子句
        }
        if (order != null) {
            if (order.equals("quantity")) { //前台必须传入↓ 评价人数排序
                queryWrapper.orderByDesc("evaluation_quantity");//降序排序
            } else if (order.equals("score")) {
                queryWrapper.orderByDesc("evaluation_score");//评分降序
            }
        }
        IPage<Book> pageObject = bookMapper.selectPage(p, queryWrapper);//传入两个参数: page对象哪一页数据  每页记录数[条件构造器]
        return pageObject;
    }
}
com/imooc/reader/controller/BookController.java
package com.imooc.reader.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.imooc.reader.entity.Book;
import com.imooc.reader.entity.Category;
import com.imooc.reader.service.BookService;
import com.imooc.reader.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import java.util.List;

//30.显示之前写好的index.ftl
@Controller
public class BookController {
    //传入模板数据 分类信息
    @Resource
    private CategoryService categoryService;
    @Resource
    private BookService bookService;
    //showIndex与url绑定  31继续向前推进因为index.ftl所有数据都是静态写死的
    //31要对每一个分类进行读取 List标签<#list>读取 转至index.ftl 64行

    /**
     * 显示首页
     *
     * @return
     */
    @GetMapping("/")
    public ModelAndView showIndex() {
        ModelAndView mav = new ModelAndView("index");
        List<Category> categoryList = categoryService.selectAll();//拿到列表
        //结果放入其中 属性名,值
        mav.addObject("categoryList", categoryList);
        return mav;
    }
//37.编写一个page方法 上面加个@Resource 绑定一个Ajax @GetMapping
    //http://localhost/books 非常长的JSON序列化结果  38index.ftl 40行

    /**
     * 分页查询图书列表
     *
     * @param p 页号
     * @return 分页对象
     */
    //48.更改调用参数 更改BookServiceImplTest.java调用参数 49回到index.ftl 增加function第二个
    @GetMapping("/books")
    @ResponseBody //具体数据来自客户端传入
    public IPage<Book> selectBook(Long categoryId, String order, Integer p) {
        if (p == null) {
            p = 1;
        }
        IPage<Book> pageObject = bookService.paging(categoryId, order, p, 10);
        return pageObject;
    }
}
index.ftl
<script>
        <#-- 41.引入星星图片 和 评价方法 上面的script运用简便写法 42下面 $(".stars")将星星的span标签选中 raty转换成可视的星星组件(被注释)-->
        $.fn.raty.defaults.path = "./resources/raty/lib/images"
        // 45.对两次Ajax的代码进行重构与梳理(定义一个loadMore()方法将下面的裁切进来底下写进递归)
        // isReset参数设置true,代表从第一页开始查询,否则按nextPage查询后续页
        // 对下面那个data: {p: 1}, 进行重构 ↓↓↓↓ isReset 下面代码已经被注释但未完全删除
        // loadMore()加载更多数据 46下方整理点击时的高亮显示 [全部]
        function loadMore(isReset){
            if(isReset == true){
                $("#bookList").html("");
                $("#nextPage").val(1);
            }
            // 50.让Ajax获取数据在下方data填写 下方$(".category和.order")都要重新调用递归
            // 细节清空再显示 上方$("#bookList").html("");
            var nextPage = $("#nextPage").val();
            var categoryId= $("#categoryId").val();
            var order = $("#order").val();
            //接下来就是发送数据时 组织成参数发送服务器
            $.ajax({
                url : "/books" ,
                //nextPage为2可以加载第二页  ↓字符串:变量
                data : {p:nextPage,"categoryId":categoryId , "order":order},
                type : "get" ,
                dataType : "json" ,
                success : function(json){
                    console.info(json);
                    var list = json.records;
                    for(var i = 0 ; i < list.length ; i++){
                        var book = json.records[i];
                        // var html = "<li>" + book.bookName + "</li>";
                        //将数据结合tpl模板,生成html
                        var html = template("tpl" , book);
                        console.info(html);
                        //jquery的id选择器选中div对象 追加
                        $("#bookList").append(html);
                    }
                    $(".stars").raty({readOnly:true});
                    // 44.如果当前页小于总页数 下面利用val设置隐藏域的值
                    // 可能将按照字符串处理 结果是31而不是4 要强制转换
                    // 若有后续数据的话 最后几行的divNoMore需要隐藏起来
                    // 45对两次的Ajax代码进行重构与梳理 上面找到第一个script
                    if(json.current < json.pages){
                        $("#nextPage").val(parseInt(json.current) + 1);
                        $("#btnMore").show();
                        $("#divNoMore").hide();
                    }else{
                        $("#btnMore").hide();
                        $("#divNoMore").show();
                    }
                }
            })
        }

图书详情页-读取图书信息

img标签src引用网络图片,响应403的解决方法_img src引用其他网站图片-CSDN博客

com/imooc/reader/service/BookService.java
public IPage<Book> paging(Long categoryId, String order, Integer page, Integer rows);
    /**
     * 根据图书编号查询图书对象
     * @param bookId 图书编号
     * @return 图书对象
     */
    //51.增加一个查询书籍的接口 再去BookServiceImpl中实现
    public Book selectById(Long bookId);
}
com/imooc/reader/service/impl/BookServiceImpl.java
 //51实现后再向上推进 BookController
    @Override
    public Book selectById(Long bookId) {
        Book book = bookMapper.selectById(bookId);
        return book;
    }
com/imooc/reader/controller/BookController.java
//51.进行页面的绑定 获取读书编号 52把所有ftl相对路径改成绝对路径 ./前面的.删掉
    //加上了./就是  localhost/book/......   删掉就是 localhost/......
    //52更改detail.ftl将静态页面改成动态页面
    @GetMapping("/book/{id}") //↓ 路径变量 和 上面的保持一致
    public ModelAndView showDetail(@PathVariable("id") Long id){
        Book book = bookService.selectById(id);
        ModelAndView mav = new ModelAndView("/detail");
        mav.addObject("book", book);
        return mav;
    }
detail.ftl
<#-- 52.更改detail.ftl将静态页面改成动态页面 <title> 还有下面图书细节 -->
<#--  53.来源于数据底层的description描述字段   54显示动态评论列表 数据库有evaluation表 创建一个entity/Evaluation-->

    <meta charset="UTF-8">
    <meta name="referrer" content="no-referrer"> <#--防止网页图片加载不出来-->
    <title>${book.bookName}</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
    <link rel="stylesheet" href="/resources/bootstrap/bootstrap.css">
    <link rel="stylesheet" href="/resources/raty/lib/jquery.raty.css">
    <script src="/resources/jquery.3.3.1.min.js"></script>
    <script src="/resources/bootstrap/bootstrap.min.js"></script>
    <script src="/resources/art-template.js"></script>
    <script src="/resources/raty/lib/jquery.raty.js"></script>

-------------------------------------------------------------------
<div class="row p-2 description">
        <#-- 53.来源于数据底层的description描述字段     -->
        ${book.description}
    </div>

图书详情页-显示评论列表

com/imooc/reader/entity/Member.java
//59.创建Member实体后完成MemberMapper接口
@TableName("member")
public class Member {
    @TableId(type = IdType.AUTO)
    private Long memberId;
    private String username;
    private String password;
    private Integer salt;
    private String nickname;
    private Date createTime;
}Getter+Setter
com/imooc/reader/entity/Evaluation.java
package com.imooc.reader.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.util.Date;
//54.创造了评论相关的底层entity 55定义一个EvaluationMapper.java接口
//再定义一个 evaluation.xml  再service上创建一个新的接口EvaluationService.java
@TableName("evaluation")
public class Evaluation {
//  id是主键
    @TableId(type= IdType.AUTO)
    private Long evaluationId;
    private Long bookId;
    private String content;
    private Integer score;
    private Long memberId;
    private Date createTime;
    private Integer enjoy;
    private String state;
    private String disableReason;
    private Date disableTime;
    @TableField(exist = false) //58.说明book属性没有对应字段,不会参与到sql自动生成
    private Book book; //Book的关联字段 加注解不存在 不会参与sql语句自动生成中 59创建会员entity对象Member实体
    @TableField(exist = false)
    private Member member; //59.获取集合后查询每个评论的信息 EvaluationServiceImpl中
}Getter + Setter
com/imooc/reader/mapper/EvaluationMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Evaluation;
//55
public interface EvaluationMapper extends BaseMapper<Evaluation> {
}
mappers/evaluation.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.EvaluationMapper">
<!--55-->
</mapper>
com/imooc/reader/service/EvaluationService.java
package com.imooc.reader.service;

import com.imooc.reader.entity.Evaluation;

import java.util.List;
//55.再创建一个它的实现类 Alt+回车 com/imooc/reader/service/impl/EvaluationServiceImpl.java
public interface EvaluationService{
    /**
     * 按图书编号查询有效短评
     * @param bookId 图书编号
     * @return 评论列表
     */
    public List<Evaluation> selectByBookId(Long bookId);
}
com/imooc/reader/service/impl/EvaluationServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Book;
import com.imooc.reader.entity.Evaluation;
import com.imooc.reader.entity.Member;
import com.imooc.reader.mapper.BookMapper;
import com.imooc.reader.mapper.EvaluationMapper;
import com.imooc.reader.mapper.MemberMapper;
import com.imooc.reader.service.EvaluationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;
//55.按图书编号查询有效短评   56BookController得到对应的图书编号以后 基于service查询对应短评信息
@Service("evluationService")
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public class EvaluationServiceImpl implements EvaluationService {
    @Resource
    private EvaluationMapper evaluationMapper;
    //59
    @Resource
    private MemberMapper memberMapper;
    @Resource
    private BookMapper bookMapper;
    /**
     * 按图书编号查询有效短评
     * @param bookId 图书编号
     * @return
     */
    @Override
    public List<Evaluation> selectByBookId(Long bookId) {
        Book book = bookMapper.selectById(bookId); //59根据参数查询book对象
        QueryWrapper<Evaluation> queryWrapper = new QueryWrapper<Evaluation>();
        queryWrapper.eq("book_id", bookId);
        queryWrapper.eq("state","enable");
        queryWrapper.orderByDesc("create_time");
        List<Evaluation> evaluationList = evaluationMapper.selectList(queryWrapper);
        //59.查询每个评论的信息
        for(Evaluation eva:evaluationList){
            Member member = memberMapper.selectById(eva.getMemberId());//59获得会员对象要使用接口咯
            eva.setMember(member);
            eva.setBook(book);//59循环的时候为每一个Evaluation设置一个book对象 60回到detail.ftl
        }
        return evaluationList;
    }
}
detail.ftl
 <div class="alert alert-primary w-100 mt-2" role="alert">短评
        <button type="button" id="btnEvaluation" class="btn btn-success btn-sm text-white float-right"
                style="margin-top: -3px;">
            写短评
        </button>
    </div>
    <div class="reply pl-2 pr-2">
<#--  57.对短评进行动态页面的整合与梳理 进行循环遍历  58注意下面的慕粉-126对应着数据库的会员表 再重来一遍 Evaluation-->
        <#list evaluationList as evaluation>
            <div>
                <div>
                    <span class="pt-1 small text-black-50 mr-2">${evaluation.createTime?string('MM-dd')}</span>
<#--   60.修改 慕粉-126 变为动态的-->
                    <span class="mr-2 small pt-1">${evaluation.member.nickname}</span>
                    <span class="stars mr-2" data-score="${evaluation.score}"></span>

                    <button type="button" data-evaluation-id="${evaluation.evaluationId}"
                            class="btn btn-success btn-sm text-white float-right" style="margin-top: -3px;">
                        <img style="width: 24px;margin-top: -5px;" class="mr-1"
                             src="https://img3.doubanio.com/f/talion/7a0756b3b6e67b59ea88653bc0cfa14f61ff219d/pics/card/ic_like_gray.svg"/>
                        <span>${evaluation.enjoy}</span>
                    </button>
                </div>

                <div class="row mt-2 small mb-3">
                    ${evaluation.content}
                </div>
                <hr/>
            </div>
        </#list>
    </div>

会员注册与登录

Kaptcha验证码的配置与使用
  • Kaptcha 是谷歌开源的可高度配置的使用验证码生成工具
  • 通过 Kaptcha 可阻拦大多数机器人脚本操作
  • Kaptcha 典型应用于注册、登录、重要信息提交等用户交互
Kaptcha 使用步骤
  • Kaptcha 配置验证码生成参数
  • 开发 KaptchaController 生成验证码图片
  • 将前台输入验证码与session保存的验证码进行比对
pom.xml
<!--   61.增加Kaptcha验证码组件依赖 applicationContext增加一个bean  -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
applicationContext.xml
<!--  61.配置Kaptcha 的bean  62创造KaptchaController.java-->
    <bean id="KaptchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
        <property name="config">
            <bean class="com.google.code.kaptcha.util.Config">
                <constructor-arg>
                    <props>
                        <!-- 验证码图片不生成边框 -->
                        <prop key="kaptcha.border">no</prop>
                        <!-- 验证码图片宽度为120像素  -->
                        <prop key="kaptcha.image.width">120</prop>
                        <!-- 验证码图片字体颜色为蓝色  -->
                        <prop key="kaptcha.textproducer.font.color">blue</prop>
                        <!-- 每个字符最大占用40像素  -->
                        <prop key="kaptcha.textproducer.font.size">40</prop>
                        <!-- 验证码包含4个字符  -->
                        <prop key="kaptcha.textproducer.char.length">4</prop>
                    </props>
                </constructor-arg>
            </bean>
        </property>
    </bean>
com/imooc/reader/controller/KaptchaController.java
package com.imooc.reader.controller;

import com.google.code.kaptcha.Producer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
//62.完成图片的随机生成与控制台的输出相同 63将原型里的 注册页 复制到项目工程register.ftl
@Controller
public class KaptchaController {
    @Resource
    private Producer kaptchaProducer;
//    Ioc动态注入 因为验证码组件设计的时候没有考虑过SpringMVC的集成 要使用原生的请求与响应
    @GetMapping("/verify_code")
    public void createVerifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //响应立即过期
        response.setDateHeader("Expires",0);
        //不存储 不缓存任何图片数据
        response.setHeader("Cache-Control","no-store,no-cache,must-revalidate");
        response.setHeader("Cache-Control","post-check=0,pre-check=0");
        response.setHeader("Pragma","no-cache");
        response.setContentType("image/png");
        //生成验证码字符文本
        String verifyCode = kaptchaProducer.createText();
        request.getSession().setAttribute("kaptchaVerifyCode",verifyCode);
        System.out.println( request.getSession().getAttribute("kaptchaVerifyCode"));
        BufferedImage image = kaptchaProducer.createImage(verifyCode);//创建验证码图片 二进制图片
        //二进制用getOutputStream() 字符用getWritter()
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image,"png",out);//输出图片流
        out.flush(); //立即输出
        out.close(); //关闭输出流

    }
}

Kaptcha验证码的使用与对比 [运用到前端]

register.ftl
<div class="col-4 p-0 pl-2 pt-0">
   <!-- 63.验证码图片 src要显示从服务器后台动态生成的验证码 KaptchaController生成的url粘贴在这里 64弄一个MemberController-->
   <img id="imgVerifyCode" src="/verify_code"
   style="width: 120px;height:50px;cursor: pointer">
</div>
com/imooc/reader/controller/MemberController.java
package com.imooc.reader.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

//64.嵌入验证码 65编写单机验证码刷新register.ftl中的function reloadVerifyCode()
@Controller
public class MemberController {
    @GetMapping("/register.html")
    public ModelAndView showRegister() {
        return new ModelAndView("/register");
    }
    //67. 验证码匹配比对 request拿到对象  68编写会员服务MemberService.java
    @PostMapping("/registe")
    @ResponseBody
    public Map registe(String vc, String username, String password, String nickname, HttpServletRequest request){
        //接收kaptchaController.java中的request.getSession().setAttribute响应
        //正确验证码
        String verityCode  = (String)request.getSession().getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)){
            result.put("code","VC01");
            result.put("msg","验证码错误");
        }else {
            result.put("code","0");
            result.put("msg","success");
        }
        return result;
    }
}
register.ftl
<script>
    //控制错误信息的显示与隐藏
    function showTips(isShow, css, text) {
        if (isShow) {
            $("#tips").removeClass("d-none")
            $("#tips").hide();
            $("#tips").addClass(css);
            $("#tips").text(text);
            $("#tips").fadeIn(200);
        } else {
            $("#tips").text("");
            $("#tips").fadeOut(200);
            $("#tips").removeClass();
            $("#tips").addClass("alert")
        }
    }
    //65.重新发送请求,刷新验证码 66验证码与后台绑定[比对校验]
    function reloadVerifyCode(){
        //请在这里实现刷新验证码 ts timestamp是时间戳 增加时间戳取消缓存
        $("#imgVerifyCode").attr("src","/verify_code?ts=" + new Date().getTime())
    }
    
    //点击验证码图片刷新验证码
    $("#imgVerifyCode").click(function () {
        reloadVerifyCode();
    });
    
    // 66.点击提交按钮,向/registe发起ajax请求
    //提交请求包含四个参数
    //vc:前台输入验证码  username:用户名 password:密码 nickname:昵称
    $("#btnSubmit").click(function () {
        //表单校验
        var username = $.trim($("#username").val());
        var regex = /^.{6,10}$/;
        if (!regex.test(username)) {
            showTips(true, "alert-danger", "用户名请输入正确格式(6-10位)");
            return;
        } else {
            showTips(false);
        }

        var password = $.trim($("#password").val());

        if (!regex.test(password)) {
            showTips(true, "alert-danger", "密码请输入正确格式(6-10位)");
            return;
        } else {
            showTips(false);
        }

        $btnReg = $(this);

        $btnReg.text("正在处理...");
        $btnReg.attr("disabled", "disabled");
        
        //66.发送ajax请求 67编写MemberController.java实现url
        $.ajax({
            url: "/registe",
            type: "post",
            dataType: "json",
            data: $("#frmLogin").serialize(),
            success: function (data) {
                //结果处理,根据服务器返回code判断服务器处理状态
                //服务器要求返回JSON格式:
                //{"code":"0","msg":"处理消息"}
                console.info("服务器响应:" , data);
                if (data.code == "0") {
                    //显示注册成功对话框
                    $("#exampleModalCenter").modal({});
                    $("#exampleModalCenter").modal("show");
                } else {
                    //服务器校验异常,提示错误信息
                    showTips(true, "alert-danger", data.msg);
                    reloadVerifyCode();
                    $btnReg.text("注    册");
                    $btnReg.removeAttr("disabled");
                }
            }
        });
        return false;
    });
</script>
============================
67在上面MemberController.java

实现会员注册功能

500 (Internal Server Error)_谷歌浏览器 internal server error-CSDN博客

com/imooc/reader/service/MemberService.java
package com.imooc.reader.service;

import com.imooc.reader.entity.Member;
//68.创建一个实现类MemberServiceImpl.java[Alt + Enter]
public interface MemberService {
    /**
     * 会员注册,创建新会员
     * @param username
     * @param password
     * @param nickname
     * @return
     */
    public Member createMember(String username, String password, String nickname);
}
com/imooc/reader/service/exception.java
package com.imooc.reader.service.exception;

/**
 * 68.业务逻辑异常 回到MemberServiceImpl编写用户名异常 69返回MemberServiceImpl
 */
public class BussinessException extends RuntimeException{
    private String code;
    private String msg;

    //构造方法
    public BussinessException(String code, String msg) {
        super(msg); //继承自运行时异常
        this.code = code;
        this.msg = msg;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}
com/imooc/reader/utils/MD5Utils.java
package com.imooc.reader.utils;

import org.apache.commons.codec.digest.DigestUtils;
//70.生成MD5方法 返回MemberServiceImpl编写随机数
public class MD5Utils {
    public static String md5Digest(String source, Integer salt){
        char[] ca = source.toCharArray();//获取字符数组
        for (int i = 0; i < ca.length; i++) {
            ca[i] = (char) (ca[i] + salt);
        }
        String target = new String(ca);
        String md5 = DigestUtils.md5Hex(target);
        return md5;
    }
}
com/imooc/reader/service/impl/MemberServiceImpl.java
package com.imooc.reader.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imooc.reader.entity.Member;
import com.imooc.reader.mapper.MemberMapper;
import com.imooc.reader.service.exception.BussinessException;
import com.imooc.reader.service.MemberService;
import com.imooc.reader.utils.MD5Utils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Random;

//68.完成与会员的交互[写操作居多] 完成运行异常service/BussinessException.java
@Service("memberService")
@Transactional
public class MemberServiceImpl implements MemberService {
    @Resource
    private MemberMapper memberMapper;

    /**
     * 会员注册,创建新会员
     *
     * @param username 用户名
     * @param password 密码
     * @param nickname 昵称
     * @return 新会员对象
     */
    public Member createMember(String username, String password, String nickname) {
        QueryWrapper<Member> queryWrapper = new QueryWrapper<Member>();
        queryWrapper.eq("username", username);
        List<Member> memberList = memberMapper.selectList(queryWrapper);
        //判断用户名是否已存在 68定义一个异常BussinessException.java
        //69.续写异常  70增加加密组件pom.xml
        if (memberList.size() > 0) {
            throw new BussinessException("M01", "用户名已存在");
        }
        Member member = new Member();
        member.setUsername(username);
        member.setNickname(nickname);
        //70. 随机数md5  71生成测试用例MemberServiceImplTest
        int salt = new Random().nextInt(1000)+1000; //盐值
        String md5 = MD5Utils.md5Digest(password, salt);
        member.setPassword(md5);
        member.setSalt(salt);
        member.setCreateTime(new Date()); //创建时间
        memberMapper.insert(member);
        return member;
    }
}
com/imooc/reader/service/impl/MemberServiceImplTest.java
package com.imooc.reader.service.impl;

import com.imooc.reader.service.MemberService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

//71.生成测试用例 72MemberController后续的工作
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class MemberServiceImplTest {
    @Resource
    private MemberService memberService;
    @Test
    public void createMember() {
        memberService.createMember("s123457", "123456", "测试");
    }
}
com/imooc/reader/controller/MemberController.java
package com.imooc.reader.controller;

import com.imooc.reader.service.exception.BussinessException;
import com.imooc.reader.service.MemberService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

//64.嵌入验证码 65编写单机验证码刷新register.ftl中的function reloadVerifyCode()
@Controller
public class MemberController {
    //72后续工作 进行注入
    @Resource
    private MemberService memberService;

    @GetMapping("/register.html")
    public ModelAndView showRegister() {
        return new ModelAndView("/register");
    }

    //67. 验证码匹配比对 request拿到对象  68编写会员服务MemberService.java
    @PostMapping("/registe")
    @ResponseBody
    public Map registe(String vc, String username, String password, String nickname, HttpServletRequest request) {
        //接收kaptchaController.java中的request.getSession().setAttribute响应
        //正确验证码
        String verityCode = (String) request.getSession().getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)) {
            result.put("code", "VC01");
            result.put("msg", "验证码错误");
        } else {
            //72.调用过程  73将登录页复制进去 74MemberServiceImpl
            try {
                memberService.createMember(username, password, nickname);
                result.put("code", "0");
                result.put("msg", "success");
            } catch (BussinessException ex) {
                ex.printStackTrace();
                result.put("code", ex.getCode());
                result.put("msg", ex.getMsg());
            }
        }
        return result;
    }
}
http://localhost/register.html

实现会员登录功能

 com/imooc/reader/service/impl/MemberServiceImpl.java
 /**
     * 登陆检查
     * @param username 用户名
     * @param password 密码
     * @return 登录对象
     */
    //74.登录方法的接口实现 校验密码 75再续方法 MemberController.java
    public Member checkLogin(String username, String password) {
        QueryWrapper<Member> queryWrapper = new QueryWrapper<Member>();
        queryWrapper.eq("username", username);
        Member member = memberMapper.selectOne(queryWrapper);
        if (member == null){
            throw new BussinessException("M02", "用户不存在");
        }//校验密码
        String md5 = MD5Utils.md5Digest(password, member.getSalt());
        if (!md5.equals(member.getPassword())){
            throw new BussinessException("M03", "输入密码有误");
        }
        return member;
    }
com/imooc/reader/controller/MemberController.java
// 75.HttpSession session 登录校验后会将这个会员信息存放到session中
    // 76登录校验成功以后会返回一个member对象 存放在session中
    @PostMapping("/check_login") //前台url
    @ResponseBody
    public Map checkLogin(String username, String password, String vc, HttpSession session){
        //正确验证码                     ↓为了得到session对象
        String verityCode = (String)session.getAttribute("kaptchaVerifyCode");
        //验证码比对  后面的是大小写对比
        Map result = new HashMap();
        if (vc == null || verityCode == null || !vc.equalsIgnoreCase(verityCode)) {
            result.put("code", "VC01");
            result.put("msg", "验证码错误");
        }else {
            try {//处理成功
                Member member = memberService.checkLogin(username, password);
                //76.member存放在session中  77去index.ftl下方登录块改造
                session.setAttribute("loginMember",member);
                result.put("code", "0");
                result.put("msg", "success");
            } catch (BussinessException ex) { //处理失败
                ex.printStackTrace();
                result.put("code", ex.getCode());
                result.put("msg", ex.getMsg());
            }
        }
        return result;
    }
index.ftl 
<#--   77.将获取到的member的session数据替换成登录右上角的动态数据-->
  <#--  两个问号代表前面的属性是存在的情况下输出其中的html -->
<#-- 78新增entity/MemberReadState.java会员阅读状态 -->
        <#if loginMember??>
            <h6 class="mt-1">
                <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">${loginMember.nickname}
            </h6>
            <#else> <#--上面是已登录 下面是未登录-->
                <a href="/login.html" class="btn btn-light btn-sm">
                    <img style="width: 2rem;margin-top: -5px" class="mr-1" src="./images/user_icon.png">登录
                </a>
        </#if>

获取会员阅读状态

实现会员交互功能
  • 想看/看过的阅读状态变更
  • 为图书写短评
  • 为喜欢的短评点赞
com/imooc/reader/entity/MemberReadState.java
package com.imooc.reader.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

import java.util.Date;

/**
 * 会员阅读状态实体
 */
//78.搞实体 再去创MemberReadStateMapper.java接口
public class MemberReadState {
    @TableId(type = IdType.AUTO)
    private Long rsId;
    private Long bookId;
    private Long memberId;
    private Integer readState;
    private Date createTime;
}
com/imooc/reader/mapper/MemberReadStateMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.MemberReadState;

//78接口创完 再去搞个Member_Read_State.xml
public interface MemberReadStateMapper extends BaseMapper<MemberReadState> {
}
mappers/member_read_state.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.reader.mapper.MemberReadStateMapper">
<!--  78. 去MemberService 阅读状态-->
</mapper>
com/imooc/reader/service/MemberService.java 
//78.获取阅读状态 再去实现这个方法

    /**
     * 获取阅读状态
     *
     * @param memberId 会员编号
     * @param bookId   图书编号
     * @return 阅读状态对象
     */
    public MemberReadState selectMemberReadState(Long memberId, Long bookId);
com/imooc/reader/service/impl/MemberServiceImpl.java
 //78.新增方法实现那个方法 再去上方进行方法注入 79根据会员编号进行查询BookController

    /**
     * 获取阅读状态
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @return 阅读状态对象
     */
    public MemberReadState selectMemberReadState(Long memberId, Long bookId){
        QueryWrapper<MemberReadState> queryWrapper = new QueryWrapper();
        queryWrapper.eq("book_id", bookId);
        queryWrapper.eq("member_id", memberId);
        MemberReadState memberReadState = memberReadStateMapper.selectOne(queryWrapper);
        return memberReadState;
    }
com/imooc/reader/controller/BookController.java
//51.进行页面的绑定 获取读书编号 52把所有ftl相对路径改成绝对路径 ./前面的.删掉
    //加上了./就是  localhost/book/......   删掉就是 localhost/......
    //52更改detail.ftl将静态页面改成动态页面
    @GetMapping("/book/{id}") //↓ 路径变量 和 上面的保持一致
    //79.新增一个参数 获取当前用户登录信息
    public ModelAndView showDetail(@PathVariable("id") Long id, HttpSession session) {
        Book book = bookService.selectById(id);
        //56.写完并放入 mav.addObject 57得到了对象就要去detail.ftl渲染循环遍历[写短评下方的div]
        List<Evaluation> evaluationList = evaluationService.selectByBookId(id);
        //79.将之前设置的用户信息拿出来 两种情况 ①会员没登录就null ②会员登录了member对象存在了 注入memberService
        Member member = (Member)session.getAttribute("loginMember");
        ModelAndView mav = new ModelAndView("/detail");
        if (member != null) { //获取会员阅读状态    会员编号 图书编号  80 detail.ftl想看[1]与看过[2] 页面上方找script块
            MemberReadState memberReadState = memberService.selectMemberReadState(member.getMemberId(), id);
            mav.addObject("memberReadState", memberReadState);

        }
        mav.addObject("book", book);
        mav.addObject("evaluationList", evaluationList);
        return mav;
    }
detail.ftl
 <script>
        $.fn.raty.defaults.path = '/resources/raty/lib/images';
        $(function () {
            $(".stars").raty({readOnly: true});
        })
       // 80 想看与看过 freemark脚本 如果它存在(想看/看过) 81产生对应的状态数据 MemberService
        $(function () {
            <#if memberReadState ??>
            // 重选阅读状态回填
            $("*[data-read-state='${memberReadState.readState}']").addClass("highlight");
            </#if>
            <#if !loginMember ??>
                $("*[data-read-state]").click(function () {
                    // 利用jquery选择div的对话框函数 显示需要登录
                    $("#exampleModalCenter").modal("show");
                })
            </#if>
        })
    </script>

更新会员阅读状态

com/imooc/reader/service/MemberService.java
 /**
     * 更新阅读状态
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @param readState 阅读状态
     * @return 阅读对象状态
     */         //81定义方法 MemberServiceImpl实现状态
    public MemberReadState updateMemberReadState(Long memberId, Long bookId, Integer readState);
com/imooc/reader/service/impl/MemberServiceImpl.java
/**
 * 更新阅读状态
 * @param memberId 会员编号
 * @param bookId 图书编号
 * @param readState 阅读状态
 * @return 阅读对象状态
 */
//81.编写阅读状态代码 编写完打开MemberController
public MemberReadState updateMemberReadState(Long memberId, Long bookId, Integer readState){
    QueryWrapper<MemberReadState> queryWrapper = new QueryWrapper<MemberReadState>();
    queryWrapper.eq("book_id", bookId);
    queryWrapper.eq("member_id", memberId);
    MemberReadState memberReadState = memberReadStateMapper.selectOne(queryWrapper);
    //空的代表没有点过按钮 数据是空的
        //无则新增,有则更新
        if(memberReadState == null){
            memberReadState = new MemberReadState();
            memberReadState.setMemberId(memberId);
            memberReadState.setBookId(bookId);
            memberReadState.setReadState(readState);
            memberReadState.setCreateTime(new Date());
            memberReadStateMapper.insert(memberReadState);
        }else{
            memberReadState.setReadState(readState);
            memberReadStateMapper.updateById(memberReadState);
        }

        return memberReadState;
    }
com/imooc/reader/controller/MemberController.java
//81. 完成更新状态的事务  82去完成detail.ftl后面的loginMember存在的代码
    @PostMapping("/update_read_state")
    @ResponseBody
    public Map updateReadState(Long memberId, Long bookId, Integer readState){
        Map result = new HashMap();
        try {
            memberService.updateMemberReadState(memberId, bookId, readState);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
//82 完成登录状态
            <#if loginMember ??>
            /**
             * 更新会员阅读状态
             */
            $("*[data-read-state]").click(function () {
                //会员阅读状态
                var readState = $(this).data("read-state");
                //发送请求
                $.post("/update_read_state", {
                    memberId: ${loginMember.memberId},
                    bookId: ${book.bookId},
                    readState: readState
                }, function (json) {
                    if (json.code == "0") { //服务器处理成功
                        $("*[data-read-state]").removeClass("highlight");//高亮的清除
                        $("*[data-read-state='" + readState + "']").addClass("highlight");//状态值放入其中
                    }
                }, "json")
            });
            </#if>

实现写短评功能

detail.ftl
 //83.短评功能  下面短评联动现象
            $("#btnEvaluation").click(function () {
                // 选中id=score的标签 转换为星型组件
                $("#score").raty({});
                $("#dlgEvaluation").modal("show");//显示短评对话框
            })
---------------------------------------------------------------
<!-- Modal -->
<div class="modal-content">
            <div class="modal-body">
<#-- 83. 短评联动   84 MemberService.java 短评实现代码 -->
                <h6>为${book.bookName}写短评</h6>
                <form id="frmEvaluation">
                    <div class="input-group  mt-2 ">
                        <span id="score"></span>
                    </div>
                    <div class="input-group  mt-2 ">
                        <input type="text" id="content" name="content" class="form-control p-4"
                               placeholder="这里输入短评">
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" id="btnSubmit" class="btn btn-primary">提交</button>
            </div>
        </div>
com/imooc/reader/service/MemberService.java
//84.短评代码!  MemberServiceImpl 组一个全新的对象
    public Evaluation evaluate(Long memberId, Long bookId, Integer score, String content);
com/imooc/reader/service/impl/MemberServiceImpl.java
/**
     * 发布新的短评
     * @param memberId 会员编号
     * @param bookId 图书编号
     * @param score 评分
     * @param content 短评内容
     * @return
     */ // 84 完成评论信息的更新 85在MemberController编写与web交互功能
    public Evaluation evaluate(Long memberId, Long bookId, Integer score, String content) {
        Evaluation evaluation = new Evaluation();
        evaluation.setMemberId(memberId);
        evaluation.setBookId(bookId);
        evaluation.setScore(score);
        evaluation.setContent(content);
        evaluation.setCreateTime(new Date());
        evaluation.setState("enable");
        evaluation.setEnjoy(0); //注入evaluationMapper 完成数据新增
        evaluationMapper.insert(evaluation);
        return evaluation;
    }
com/imooc/reader/controller/MemberController.java
//85.与web交互功能 前台请求传入数据 86回到detail.ftl写
    @PostMapping("/evaluate")
    @ResponseBody
    public Map evaluate(Long memberId, Long bookId, Integer score, String content){
        Map result = new HashMap();
        try {
//    Evaluation eva = memberService.evaluate(memberId, bookId, score, content);
            memberService.evaluate(memberId, bookId, score, content);
            result.put("code", "0");
            result.put("msg", "success");
//          result.put("evaluation", eva);
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
//86.评论对话框提交数据  87完成点赞核心实现 MemberService
            $("#btnSubmit").click(function () {
                var score = $("#score").raty("score");//获取评分
                var content = $("#content").val();
                if (score == 0 || $.trim(content) == ""){ //没有进行选择 或 删除前后空格
                    return; //禁止提交方法中断
                }
                $.post("/evaluate",{
                    score : score,
                    bookId: ${book.bookId},
                    memberId: ${loginMember.memberId},
                    content: content
                },function (json) {
                    if (json.code == "0") {//处理成功
                        window.location.reload();//列表进行刷新
                    }
                },"json")
            })

完成会员短评点赞核心实现

com/imooc/reader/service/MemberService.java
 /**
     * 短评点赞
     * @param evaluationId 短评编号
     * @return 短评对象
     */
//    87完成点赞核心实现 MemberServiceImpl
    public Evaluation enjoy(Long evaluationId);
com/imooc/reader/service/impl/MemberServiceImpl.java
// 87.短评点赞 自增+1 Membercontroller
    public Evaluation enjoy(Long evaluationId) {
        Evaluation evaluation = evaluationMapper.selectById(evaluationId);
        evaluation.setEnjoy(evaluation.getEnjoy()+1);
        evaluationMapper.updateById(evaluation);
        return evaluation;
    }
com/imooc/reader/controller/MemberController.java
 //87.实现controller 88让客户端页面发送ajax请求
    @PostMapping("/enjoy")
    @ResponseBody
    public Map evaluate(Long evaluationId) {
        Map result = new HashMap();
        try {
//    Evaluation eva = memberService.evaluate(memberId, bookId, score, content);
            Evaluation eva = memberService.enjoy(evaluationId);
            result.put("code", "0");
            result.put("msg", "success");
            result.put("evaluation", eva); //包含最新点赞数
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }
detail.ftl
// 88.评论点赞
            $("*[data-evaluation-id]").click(function(){
                var evaluationId = $(this).data("evaluation-id");
                $.post("/enjoy",{evaluationId:evaluationId},function(json){
                    if(json.code == "0"){
                        $("*[data-evaluation-id='" + evaluationId + "'] span").text(json.evaluation.enjoy);
                    }
                },"json")
            })

Spring Task定时任务 [eg: 闹钟命令]

  • Spting Task 是 Spring 3.0后推出的定时任务模块
  • Spring Task的职责是按周期后台自动执行任务
  • Spring Task可利用Cron表达式实现灵活的定时处理
Cron表达式实例
小时 星期
0 * * * *
0,30 0-5 * * * ? 2000
0 0 9-18 ? * WED
利用Spring-Task自动计算图书评分 [所有图书评价信息重算]
mappers/book.xml
<!--    89.增加平均评分增加信息  增加接口BookMapper -->
    <update id="updateEvaluation">
        update book b SET evaluation_score = (
            select ifnull(avg(score),0) from evaluation where book_id = b.book_id and state='enable'
        ),evaluation_quantity = (
            select ifnull(count(*),0) from evaluation where book_id = b.book_id and state='enable'
        )
    </update>
com/imooc/reader/mapper/BookMapper.java
package com.imooc.reader.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imooc.reader.entity.Book;
//33.创建完Book接口  34与之对应的创建book.xml文件
//89. 更新图书评分  BookService去定义
public interface BookMapper extends BaseMapper<Book> {
    /**
     * 更新图书评分/评价数量
     */
    public void updateEvaluation();
}
com/imooc/reader/service/BookService.java
 /**
     * 更新图书评分/评价数量
     */
    //89. 定义完去impl定义实现类
    public void updateEvaluation();
}
com/imooc/reader/service/impl/BookServiceImpl.java
/**
     * 更新图书评分/评价数量  Controller直接面向Service Service面向Mapper
     */
    //89. 更新操作需要声明式事务 开启  90applicationContext.xml开启task
    @Transactional
    public void updateEvaluation() {
        bookMapper.updateEvaluation();
    }
applicationContext.xml
<!--  61.配置Kaptcha 的bean  62创造KaptchaController.java-->
    <bean id="KaptchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
        <property name="config">
            <bean class="com.google.code.kaptcha.util.Config">
                <constructor-arg>
                    <props>
                        <!-- 验证码图片不生成边框 -->
                        <prop key="kaptcha.border">no</prop>
                        <!-- 验证码图片宽度为120像素  -->
                        <prop key="kaptcha.image.width">120</prop>
                        <!-- 验证码图片字体颜色为蓝色  -->
                        <prop key="kaptcha.textproducer.font.color">blue</prop>
                        <!-- 每个字符最大占用40像素  -->
                        <prop key="kaptcha.textproducer.font.size">40</prop>
                        <!-- 验证码包含4个字符  -->
                        <prop key="kaptcha.textproducer.char.length">4</prop>
                    </props>
                </constructor-arg>
            </bean>
        </property>
    </bean>
    <!-- 90.开启Spring Task定时任务的注解模式 创建一个reader/task/ComputeTask.java-->
    <task:annotation-driven/>
</beans>
com/imooc/reader/task/ComputeTask.java
package com.imooc.reader.task;

import com.imooc.reader.service.BookService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

//90.组件注解 不确定是哪个类 但是会被扫描实例化和管理
@Component
public class ComputeTask {
    @Resource
    private BookService bookService;
//  每分钟0秒时候 执行一次Cron表达式
    @Scheduled(cron = "0 * * * * ?")
    public void updateEvaluation(){
        bookService.updateEvaluation();
        System.out.println("已更新所有图书评分");
    }
}

后台管理功能-图书管理

富文本编辑器wangEditor使用入门
  • 基于Javascript与css开发的Web富文本编辑器
test.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 91.引入wangEditor  创建WEB-INF/ftl/management/book.ftl-->
    <script src="/resources/wangEditor.min.js"></script>
</head>
<body>
<div>
    <button id="btnRead">读取内容</button>
    <button id="btnWrite">写入内容</button>
</div>
<div id="divEditor" style="width: 800px;height:600px"></div>
<script>
    var E = window.wangEditor;
    var editor = new E("#divEditor");//完成富文本编辑器初始化
    editor.create();//创建富文本编辑器,显示在页面上
    document.getElementById("btnRead").onclick = function(){
        var content = editor.txt.html();//获取编辑器现有的html内容
        alert(content);
    }
    document.getElementById("btnWrite").onclick = function(){
        var content = "<li style='color:red'>我是<b>新内容</b></li>";
        editor.txt.html(content);
    }
</script>
</body>
</html>

实现图书管理功能

在训练素材中导入图书管理页
book.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<#-- 91.创建后台图书管理控制器com/imooc/reader/controller/management/MBookController.java   -->
    <title>图书管理功能</title>
    <style>
        #dlgBook{
            padding: 10px
        }
    </style>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">

    <script src="/resources/wangEditor.min.js"></script>


    <script type="text/html" id="toolbar">
        <div class="layui-btn-container">
            <button class="layui-btn layui-btn-sm" id="btnAdd" onclick="showCreate()">添加</button>
        </div>
    </script>

</head>
<body>


<div class="layui-container">
    <blockquote class="layui-elem-quote">图书列表</blockquote>
    <!-- 数据表格 -->
    <table id="grdBook" lay-filter="grdBook"></table>
</div>
<!--表单内容-->
<div id="dialog" style="padding: 10px;display: none">
    <form class="layui-form" >
        <div class="layui-form-item">
            <!-- 图书类别 -->
            <select id="categoryId" name="categoryId" lay-verify="required" lay-filter=
            "categoryId">
                <option value=""></option>
                <option value="1">前端</option>
                <option value="2">后端</option>
                <option value="3">测试</option>
                <option value="4">产品</option>
            </select>

        </div>
        <div class="layui-form-item">
            <!-- 书名 -->
            <input type="text" id="bookName" name="bookName" required lay-verify="required" placeholder="请输入书名"
                   autocomplete="off" class="layui-input">
        </div>


        <div class="layui-form-item">
            <!-- 子标题 -->
            <input type="text" id="subTitle" name="subTitle" required lay-verify="required" placeholder="请输入子标题"
                   autocomplete="off" class="layui-input">
        </div>

        <div class="layui-form-item">
            <!-- 作者 -->
            <input type="text" id="author" name="author" required lay-verify="required" placeholder="请输入作者信息"
                   autocomplete="off" class="layui-input">
        </div>

        <div style="margin-top: 30px;font-size: 130%">图书介绍(默认第一图将作为图书封面)</div>
        <div class="layui-form-item" >
            <!-- wangEditor编辑器 -->
            <div id="editor" style="width: 100%">

            </div>
        </div>
        <!-- 图书编号 -->
        <input id="bookId" type="hidden">
        <!-- 当前表单操作类型,create代表新增 update代表修改 -->
        <input id="optype"  type="hidden">
        <div class="layui-form-item" style="text-align: center">
            <!-- 提交按钮 -->
            <button class="layui-btn" lay-submit="" lay-filter="btnSubmit">立即提交</button>
        </div>
    </form>
</div>
<script src="/resources/layui/layui.all.js"></script>
<script>

    var table = layui.table; //table数据表格对象
    var $ = layui.$; //jQuery
    var editor = null; //wangEditor富文本编辑器对象
    //初始化图书列表
    table.render({
        elem: '#grdBook'  //指定div
        , id : "bookList" //数据表格id
        , toolbar: "#toolbar" //指定工具栏,包含新增添加
        , url: "/management/book/list" //数据接口
        , page: true //开启分页
        , cols: [[ //表头
            {field: 'bookName', title: '书名', width: '300'}
            , {field: 'subTitle', title: '子标题', width: '200'}
            , {field: 'author', title: '作者', width: '200'}
            , {type: 'space', title: '操作', width: '200' , templet : function(d){
                    //为每一行表格数据生成"修改"与"删除"按钮,并附加data-id属性代表图书编号
                    return "<button class='layui-btn layui-btn-sm btn-update'  data-id='" + d.bookId + "' data-type='update' onclick='showUpdate(this)'>修改</button>" +
                        "<button class='layui-btn layui-btn-sm btn-delete'  data-id='" + d.bookId + "'   onclick='showDelete(this)'>删除</button>";
                }
            }
        ]]
    });
    //显示更新图书对话框
    //obj对应点击的"修改"按钮对象
    function showUpdate(obj){
        //弹出"编辑图书"对话框
        layui.layer.open({
            id: "dlgBook", //指定div
            title: "编辑图书", //标题
            type: 1, 
            content: $('#dialog').html(), //设置对话框内容,复制自dialog DIV
            area: ['820px', '730px'], //设置对话框宽度高度
            resize: false //是否允许调整尺寸
        })

        var bookId = $(obj).data("id"); //获取"修改"按钮附带的图书编号
        $("#dlgBook #bookId").val(bookId); //为表单隐藏域赋值,提交表单时用到

        editor = new wangEditor('#dlgBook #editor'); //初始化富文本编辑器
        editor.customConfig.uploadImgServer = '/management/book/upload' //设置图片上传路径
        editor.customConfig.uploadFileName = 'img'; //图片上传时的参数名
        editor.create(); //创建wangEditor
        $("#dlgBook #optype").val("update"); //设置当前表单提交时提交至"update"更新地址

        //发送ajax请求,获取对应图书信息
        $.get("/management/book/id/" + bookId , {} , function(json){
            //文本框回填已有数据
            $("#dlgBook #bookName").val(json.data.bookName);//书名
            $("#dlgBook #subTitle").val(json.data.subTitle); //子标题
            $("#dlgBook #author").val(json.data.author);//作者
            $("#dlgBook #categoryId").val(json.data.categoryId); //分类选项
            editor.txt.html(json.data.description); //设置图文内容
            layui.form.render();//重新渲染LayUI表单
        } , "json")



    }
    //显示新增图书对话框
    function showCreate(){
        //弹出"新增图书"对话框
        layui.layer.open({
            id: "dlgBook",
            title: "新增图书",
            type: 1,
            content: $('#dialog').html(),
            area: ['820px', '730px'],
            resize: false
        })
        //初始化wangEditor
        editor = new wangEditor('#dlgBook #editor');
        editor.customConfig.uploadImgServer = '/management/book/upload';//设置图片上传地址
        editor.customConfig.uploadFileName = 'img';//设置图片上传参数
        editor.create();//创建wangEditor

        layui.form.render(); //LayUI表单重新
        $("#dlgBook #optype").val("create");//设置当前表单提交时提交至"create"新增地址

    };

    //对话框表单提交
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
        
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
    //删除图书
    function showDelete(obj){
        //获取当前点击的删除按钮中包含的图书编号
        var bookId = $(obj).data("id");
        //利用layui的询问对话框进行确认
        layui.layer.confirm('确定要执行删除操作吗?', {icon: 3, title:'提示'}, function(index){
                    
                //确认按钮后发送ajax请求,包含图书编号
                $.get("/management/book/delete/" + bookId, {}, function (json) {
                    if(json.code=="0"){
                        //删除成功刷新表格
                        table.reload('bookList');
                        //提示操作成功
                        layui.layer.msg('数据操作成功,图书列表已刷新');
                        //关闭对话框
                        layui.layer.close(index);
                    }else{
                        //处理失败,提示错误信息
                        layui.layer.msg(json.msg);
                    }
                }, "json");
            
        });

    }

</script>
</body>
</html>
com/imooc/reader/controller/management/MBookController.java
package com.imooc.reader.controller.management;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

//http://localhost/management/book/index.html
//91.后台管理系统 编写富文本编辑器的图片上传功能book.ftl已全实现
//editor.customConfig.uploadImgServer = '/management/book/upload';//设置图片上传地址
//92提供文件上传地址的接口 pom.xml增加文件上传依赖 apache
@Controller
@RequestMapping("/management/book")
public class MBookController {
    @GetMapping("/index.html")
    public ModelAndView showBook(){
        return new ModelAndView("/management/book");
    }

    /**
     * wangEditor文件上传
     * @param file 上传文件
     * @param request 原生请求对象
     * @return
     * @throws IOException
     */
    //93. 接收提交的文件 上传的文件保存到upload目录中 代码编写
    @PostMapping("/upload")
    @ResponseBody
    public Map upload(@RequestParam("img") MultipartFile file, HttpServletRequest request) throws IOException {
        //得到上传目录
        String uploadPath = request.getServletContext().getResource("/").getPath() + "/upload/";//在out里 运行时执行获取路径
        //文件名
        String fileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        //原始文件扩展名
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//最后一次获得点
        //保存文件到upload目录
        file.transferTo(new File(uploadPath + fileName + suffix));
        Map result = new HashMap();
        result.put("errno", 0);
        result.put("data", new String[]{"/upload/" + fileName + suffix});
        return result;
    }
}
pom.xml
<!-- 92.增加文件上传依赖 SpringMVC文件上传底层依赖 93去applicationContext.xml激活-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>
applicationContext.xml
<!--  93.激活文件上传功能 回到MBookController-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>
com/imooc/reader/controller/management/MBookController.java
 /**
     * wangEditor文件上传
     * @param file 上传文件
     * @param request 原生请求对象
     * @return
     * @throws IOException
     */
    //93. 接收提交的文件 上传的文件保存到upload目录中 代码编写 94book.ftl对话框表单提交
    @PostMapping("/upload")
    @ResponseBody
    public Map upload(@RequestParam("img") MultipartFile file, HttpServletRequest request) throws IOException {
        //得到上传目录
        String uploadPath = request.getServletContext().getResource("/").getPath() + "/upload/";//在out里 运行时执行获取路径
        //文件名
        String fileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        //原始文件扩展名
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//最后一次获得点
        //保存文件到upload目录
        file.transferTo(new File(uploadPath + fileName + suffix));
        Map result = new HashMap();
        result.put("errno", 0);
        result.put("data", new String[]{"/upload/" + fileName + suffix});
        return result;
    }

实现图书新增功能

book.ftl
// 94.对话框表单提交 95增加全新方法声明BookService
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
        
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
com/imooc/reader/service/BookService.java
 //95.创建新的图书 BookServiceImpl
    public Book createBook(Book book);
com/imooc/reader/service/impl/BookServiceImpl.java
 //95.数据增加 MBookController
    @Transactional
    public Book createBook(Book book) {
        bookMapper.insert(book);
        return book;
    }
com/imooc/reader/controller/management/MBookController.java
//95.获取书籍数据
    @PostMapping("/create")
    @ResponseBody
    public Map createBook(Book book){
        Map result = new HashMap();
        try {
            book.setEvaluationQuantity(0);
            book.setEvaluationScore(0f);
            Document doc = Jsoup.parse(book.getDescription()); //一个个片段被解析
            Element img = doc.select("img").first();//选中所有标签提取第一个 获取图书详情第一图的元素对象
            String cover = img.attr("src");//获取当前元素指定值
            book.setCover(cover); //95.来自于description描述的第一幅图
            //用html解析器jsoup.jorg从前台的描述里截取图片位置 96加入依赖
            bookService.createBook(book); //此处可以打点debug
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode());
            result.put("msg", ex.getMsg());
        }
        return result;
    }

实现图书分页查询[显示图书列表]

com/imooc/reader/controller/management/MBookController.java
 //96.设计列表显示加载 97回到book.ftl
    @GetMapping("/list")
    @ResponseBody
    public Map list(Integer page, Integer limit){
        if (page == null){
            page = 1;
        }
        if (limit == null){
            limit = 10;
        }
        IPage<Book> pageObject = bookService.paging(null, null, page, limit);//返回paging的分页对象
        Map result = new HashMap();
        result.put("code", "0");
        result.put("msg", "success");
        //layUI返回值必须要求写的
        result.put("data", pageObject.getRecords()); //当前页面数据
        result.put("count", pageObject.getTotal()); //未分页时记录总数
        return result;
    }

实现图书修改更新功能 [原有数据调整 回填数据]

book.ftl
// 94.对话框表单提交 95增加全新方法声明BookService
    layui.form.on('submit(btnSubmit)', function(data){
        //获取表单数据
        var formData = data.field;
        
        //判断是否包含至少一副图片,默认第一图作为封面显示
        var description = editor.txt.html();
        if(description.indexOf("img") == -1){
            layui.layer.msg('请放置一副图片作为封面');
            return false;
        }
        //获取当前表单要提交的地址
        //如果是新增数据则提交至create
        //如果是更新数据则提交至update
        var optype = $("#dlgBook #optype").val();
     // 97.从隐藏域拿数据   98 MBookController
        if(optype == "update"){
            //更新数据时,提交时需要附加图书编号
            formData.bookId=$("#dlgBook #bookId").val();
        }
        //附加图书详细描述的图文html
        formData.description = description;
        //向服务器发送请求
        $.post("/management/book/" + optype , formData , function(json){
            if(json.code=="0"){
                //处理成功,关闭对话框,刷新列表,提示操作成功
                layui.layer.closeAll();
                table.reload('bookList');
                layui.layer.msg('数据操作成功,图书列表已刷新');
            }else{
                //处理失败,提示错误信息
                layui.layer.msg(json.msg);
            }
        } ,"json")
        return false;
    });
com/imooc/reader/controller/management/MBookController.java
//98 前台ajax发送的格式保持一致 /managemet/book/id/ + bookId
    @GetMapping("/id/{id}")
    @ResponseBody
    public Map selectById(@PathVariable("id") Long bookId) {
        Book book = bookService.selectById(bookId);
        Map result = new HashMap();
        result.put("code", "0");
        result.put("msg", "success");
        result.put("data", book); //服务器返回json.data book.ftl132行
        return result;
        //99 点击修改后的确认提交 增添更新操作 BookService.java
    }
com/imooc/reader/service/BookService.java
/**
     * 更新图书
     * @param book 新图书数据
     * @return 更新后的数据
     */
    //99.创建新的更新 BookServiceImpl
    public Book updateBook(Book book);
com/imooc/reader/service/impl/BookServiceImpl.java
/**
     * 更新图书
     * @param book 新图书数据
     * @return 更新后的数据
     */
    //99.更新数据 100 MBookController
    @Transactional
    public Book updateBook(Book book) {
        bookMapper.updateById(book);
        return null;
    }
com/imooc/reader/controller/management/MBookController.java
    /**
     * 更新图书数据
     * @param book
     * @return
     */
    //100. 更新代码 不要轻易对Book book数据直接更新 根据传入的book编号把数据库记录查出来
//101 删除方法BookService
    @PostMapping("/update")
    @ResponseBody
    public Map updateBook(Book book){
        Map result = new HashMap();
        try {
            Book rawBook = bookService.selectById(book.getBookId());
            //通过前台数据获取
            rawBook.setBookName(book.getBookName());
            rawBook.setSubTitle(book.getSubTitle());
            rawBook.setAuthor(book.getAuthor());
            rawBook.setCategoryId(book.getCategoryId());
            rawBook.setDescription(book.getDescription());
            Document doc = Jsoup.parse(book.getDescription());
            String cover = doc.select("img").first().attr("src");
            rawBook.setCover(cover);
            bookService.updateBook(rawBook);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode()); ///成功时
            result.put("msg", ex.getMsg());//失败时
        }
        return result;
    }

实现图书删除功能

http://localhost/management/book/index.html
http://localhost/

com/imooc/reader/service/BookService.java
 //101.删除方法 BookServiceImpl
    public void deleteBook(Long book);
com/imooc/reader/service/impl/BookServiceImpl.java
//101.删除相关数据 上面增加memberReadStateMapper evaluationMapper
    //一次性三个表删除 有声明式事务不会删一半 102 MBookController.java
    /**
     * 删除图书及其数据
     * @param bookId 图书编号
     */
    @Transactional
    public void deleteBook(Long bookId) {
        bookMapper.deleteById(bookId);//单独删除id效率太低 写个构造器
        QueryWrapper<MemberReadState> mrsQueryWrapper = new QueryWrapper<MemberReadState>();
        mrsQueryWrapper.eq("book_id", bookId);
        memberReadStateMapper.delete(mrsQueryWrapper);
        QueryWrapper<Evaluation> evaluationQueryWrapper = new QueryWrapper<Evaluation>();
        evaluationQueryWrapper.eq("book_id", bookId);
        evaluationMapper.delete(evaluationQueryWrapper);
    }
com/imooc/reader/controller/management/MBookController.java
//102. 封装 以及 code输出  103加入后台首页=>index.ftl 后增加ManagementController
    @GetMapping("/delete/{id}")
    @ResponseBody
    public Map deleteBook(@PathVariable("id") Long bookId) {
        Map result = new HashMap();
        try {
            bookService.deleteBook(bookId);
            result.put("code", "0");
            result.put("msg", "success");
        } catch (BussinessException ex) {
            ex.printStackTrace();
            result.put("code", ex.getCode()); ///成功时
            result.put("msg", ex.getMsg());//失败时
        }
        return result;
    }
增加后台框架

慕课书评网数据管理系统

com/imooc/reader/controller/management/ManagementController.java
package com.imooc.reader.controller.management;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
//103.后台管理系统控制器 104 修改index.ftl的静态地址
/**
 * 后台管理系统控制器
 */
@Controller
@RequestMapping("/management") //功能启动后台所需
public class ManagementController {
    @GetMapping("/index.html")
    public ModelAndView showIndex(){
        return new ModelAndView("/management/index");
    }
}
index.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>慕课书评网数据管理系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
</head>

<body class="layui-layout-body">
<!-- Layui后台布局CSS -->
<div class="layui-layout layui-layout-admin">
    <!--头部导航栏-->
    <div class="layui-header">
        <!--系统标题-->
        <div class="layui-logo" style="font-size:18px">慕课书评网数据管理系统</div>
        <!--右侧当前用户信息-->
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:void(0)">
                    <!--图标-->
                    <span class="layui-icon layui-icon-user" style="font-size: 20px">
                    </span>
                    <!--用户信息-->
                    admin
                </a>
            </li>
            <!--注销按钮-->
            <li class="layui-nav-item"><a href="/management/logout">注销</a></li>
        </ul>
    </div>
    <!--左侧菜单栏-->
    <div class="layui-side layui-bg-black">
        <!--可滚动菜单-->
        <div class="layui-side-scroll">
            <!--可折叠导航栏-->
            <ul class="layui-nav layui-nav-tree">


                    <li class="layui-nav-item layui-nav-itemed">
                        <a href="javascript:void(0)">数据管理</a>
                        <dl class="layui-nav-child module" data-node-id="xxx">
<#--       104.静态地址改为动态地址-->
                            <dd><a href="/management/book/index.html" target="ifmMain">图书管理</a></dd>
                            <dd><a href="短评管理.html" target="ifmMain">短评管理
                                </a></dd>
                        </dl>
                    </li>
            </ul>
        </div>
    </div>
    <!--主体部分采用iframe嵌入其他页面-->
    <div class="layui-body" style="overflow-y: hidden">
        <iframe name="ifmMain" style="border: 0px;width: 100%;height: 100%" src="/management/book/index.html"></iframe>
    </div>
    <!--版权信息-->
    <div class="layui-footer">
        Copyright © imooc. All Rights Reserved.
    </div>
</div>
<!--LayUI JS文件-->
<script src="/resources/layui/layui.all.js"></script>
<script>
    //将所有功能根据parent_id移动到指定模块下
    layui.$(".function").each(function () {
        var func = layui.$(this);
        var parentId = func.data("parent-id");
        layui.$("dl[data-node-id=" + parentId + "]").append(func);
    });
    //刷新折叠菜单
    layui.element.render('nav');
</script>
</body>
</html>
阅读全文

软件设计师刷题考点

2024/1/16

计算机工作原理

@@ 如果”2x”的补码是”90H”, 那么x的真值是 -56
90H 拆开根据8 4 2 1由十六进制转换为二进制:10010000;补码最高位为符号位, 1代表负号, 所以说明此数为负数,可以根据口诀直接得源码(注意符号位不变):1111 0000 即 -112;故2X=-112 X=-56

@@ 计算机中CPU对其访问速度最快的是通用寄存器
计算机系统中的CPU内部对通用寄存器的存取操作是速度最快的 其次是Cache 再次是内存 最慢的是作为外存的硬盘

@@ 属于CPU中算术逻辑单元的部位是**加法器**
控制单元部件:令寄存器、序计数器、地寄存器、据寄存器[紫橙子树] 加法器是算数逻辑运算单元的部件

@@ 在CPU中,常用来为ALU执行算数逻辑运算提供数据并暂存运算结果的寄存器是累加寄存器[是专门存放算术或逻辑运算的一个操作数和运算结果的寄存器(加 减 读出 移位)]

@@ 所谓定点数,就是表示数据时小数点的位置固定不变。
小数点的位置通常由两种约定方式:
①定点整数(纯整数, 小数点在最低有效数值位之后)
②定点小数(纯小数, 小数点在最高有效数值位之前)
当机器字长为n时, 定点数的补码和移码可表示$2^n$个数, 而其源码和反码只能表示$2^n-1$个数(0占用了两个编码), 所以定点数所能表示的数值范围比较小, 运算中很容易因结果超出范围而溢出
数的浮点表示的一般形式为:N=F×$2^E$, 其中E称为阶码,F为尾数。
尾数部分F的位数越多↑,数的精度越高↑;指数部分E的位数越多↑,能表示的范围越大↑
阶码通常为带符号的纯整数
尾数为带符号的纯小数
在尾数用补码表示时,规格化浮点数应满足尾数最高数位与符号位不同
1/2 ≤ |F|< 1时,应有0.1xx…x形式;
当**-1 ≤ M < -1/2时,应有1.0xx…x形式
M=-1/2**时,对于源码来说是规格化数,而对于补码来说不是规格化数

@@ 寄存器既可以用来存放数据地址,也可以存放控制信息CPU工作时的状态
在CPU中增加寄存器的数量,可以使CPU把执行程序时所需的数据尽可能地放在寄存器中,从而减少访问内存的次数,提高其运行速度

@@ 程序计数器(PC)用于存放下一条指令所在单元的地址的地方

@@ 计算机系统中采用补码来表示和运算数据,原因是采用补码可以简化计算机运算部件的设计
使用补码表示数据时,可以将符号位和其他位统一处理,减法也可按加法来处理,从而简化运算部件的设计

@@ 为了使一个系统能检查和纠正一个差错,间码最小距离必须至少是3
海明码是一种可以纠正一位差错的编码,是利用奇偶性来检错和纠错的一种校验方法;海明码的基本意思是給传输的数据增加r个校验位,从而增加两个合法消息的不同位的个数。海明码利用奇偶性进行检错纠错;其码距最小为2n+1 [海明校验码只有在代码中只存在一个错误的前提下,才能实现纠错 ]
**循环冗余校验码(CRC)**编码方法是在k位信息码后再拼接r位的校验码,形成长度位n位的编码,其特点是检错能力极强且开销小,用于编码器检测电路实现
奇偶校验是由若干位有效信息,再加上1 一个二进制位(校验位)组成校验码;奇偶校验只能查错不能纠错,只有奇数个数发生错误,才能发现错误;若有奇数个数据位出错,则可以检测出该错误但无法纠正错误

@@ 冗余技术:
1.结构冗余,按其工作方法可以分位静态动态混合冗余
2.信息冗余,指的是为了检测纠正信息在运算或传输中的错误另外附加的一部分信息
3.时间冗余,指的是以重复执行指令或程序来消除顺时错误带来的影响
4.冗余附件技术,指的是为实现上述冗余技术所需的资源和技术

@@ CPU依据指令周期的不同阶段来区分在内存中以二进制编码形式存放的指令和数据
指令周期是执行一条指令所需时间,一般由若干个机器周期组成,是从取指令、分析指令到执行完所需的全部时间

@@ 浮点数相加,对阶时,小数向大数看齐;对阶是通过较小数的尾数右移实现的。将小阶向大阶对齐,同时将尾数右移n位

@@ 在CPU的寄存器中,指令寄存器对用户是完全透明的

@@ 程序计数器是用于存放下一条指令所在单元的地址的地方

@@ 属于CPU中算数逻辑单元的部件是加法器

@@ 高速缓存地址是为了高速缓存的读取和写入而使用的地址,在CPU和高速缓存之间进行数据传输时使用的;当CPU需要读取数据时,DMA控制器和中断CPU发出的数据地址是主存的物理地址[中断向量提供的是中断服务程序的入口地址];硬盘的扇区地址是用于访问硬盘上指定扇区的地址,并不直接与CPU进行数据交互;虚拟地址,是指在操作系统中使用的地址,它需要通过地址映射和页表等机制转换为物理地址才能被CPU使用

@@ 相联存储器是指按内容访问的存储器
构成存储器的材料:磁存储器、半导体存储器、光存储器
按存储器的工作方式:读写存储器、只读存储器
访问方式:按地址访问的存储器、按内容访问的存储器
按**[寻]地址**访问的存储器:随机存储器、顺序存储器、直接存储器

@@ ±0编码相同的是补码和移码

@@ 计算机感染特洛伊木马后的典型现象是有未知程序试图建立网络连接。特洛伊木马是一种通过网络传播的病毒,分为客户端和服务器端两部分,服务器端位于被感染的计算机,特洛伊木马服务器端运行后会试图建立网络连接,所以计算机感染特洛伊木马后的典型现象是有未知程序试图建立网络连接

@@ 再微型计算机中,管理键盘最适合采用的1/0控制方式是中断方式

@@ 在移臂调度算法中,先来先服务最短寻找时间优先(根据当前磁臂到要请求访问磁道的距离,誰短满足誰的请求)算法可能会随时改变移动臂的运动方向

@@ 计算机中,系统总线用于CPU、主存及外设部件连接

@@ 双核是指在一个CPU中集成两个运算核心以提高运算能力

@@ 已知某高级语言源程序A经编译后得到机器C上的目标程序B,则对B进行反编译,不能还原出源程序A

@@ 在程序执行过程中,Cache与主存的地址映射是由硬件自动完成的

@@ 关于软件著作权产生的时间是自作品完成创作之日

@@ 人耳:20Hz ~ 20kHz。 说话:300 ~ 3400Hz。音乐:20 ~ 20kHz。白噪音:20 ~ 20kHz

@@ 存储动画的文件格式有FLC、GIF、SWF

@@ 网络视频格式包括MOV、RM、ASF、WMV==

@@ 多媒体视频图像文件格式有AVI、MPG、ASF、MP4

@@ 声音、音频文件格式有WAV、WMA、MP3、MIDI、RA、APE

@@ 属于图像文件格式有GIF、BMP、JPG、PNG、TIF

.wma微软公司的有损压缩,压缩比高于MP3,Win7录音机默认格式

@@ 位图(Bitmap)=> BMP

@@ 语音识别技术体现了多媒体技术人工智能技术相结合

@@ 计算机系统的主存主要是由DRAM构成的。
随机访问存储器(RAM)有两类:静态的(SRAM) 和 动态的(DRAM)

@@ 存储系统采用Cache技术的主要目的是提高存储器的访问速度,因此是由硬件自动完成Cache与主存之间的操作

@@ 主存与Cache的地址映射方式中,全相联方式可以实现主存任意一块装入Cache中任意位置,只有装满才需要替换
全相联地址映射:主存的任意一块可以映射到Cache中的任意一块
直接相联映射:主存中一块只能映射到到Cache的一个特定的块中
组相联的映射:各区中的某一块只能存入缓存的同组号的空间内,但组内各块地址之间则可以任意存放。即从主存的组到Cache的组之间采用直接映像方式,在两个对应的组内部采用全相联映像方式

@@ 常用的虚拟存储器由主存-赋存两级存储器构成
虚拟存储技术使辅助存储器和主存储器密切配合。虚拟存储器的地址称为虚地址或逻辑地址

@@ 虚拟存储体系由主存-赋存两级存储器构成
一般计算机系统中主要由两种存储体系:
Cache存储体系Cache主存储器构成,主要目的是提高存储器速度,对系统程序员以上均透明
虚拟存储体系由主存储器和在线磁盘存储器等辅存构成,主要目的是扩大存储器容量,对应用程序员透明

@@ Cache的地址映像方式中,发生块冲突次数最小的是全相联映像
Cache的地址映射方法主要有三种:全相联地址映射、直接相联地址映射、组相联地址映射
**全相联地址映射 **[意味着主存的任意一块可以映射到Cache中的任意一块,特点:块冲突概率低,Cache空间利用率高,但相联目录表容量大导致成本高、查表速度慢]
直接映像方式 [指主存的每一块只能映像到Cache的一个特定的块中,整个Cache地址与主存地址的低位部分完全相同,其特点是硬件简单,不需要相联存储器,访问速度快,Cache块冲突概率高,Cache控件利用率低]
组相联映射 [是上面两种方法的这种处理]

@@ 计算机采用分级存储体系的主要目的是为了解决存储容量、成本和速度之间的矛盾问题
高速缓存一般用SRAM[速度快,成本高]
内存一般用DRAM[集成度高,动态刷新]
外存一般用磁存储器[速度慢,容量大,价格便宜]

@@ Cache的设计思想是在合理成本下提高命中率

@@ 并行总线适合近距离高速数据传输串行总线适合长距离数据传输
在单总线结构中,CPU与主存之间、CPU与I/O设备之间、I/O设备与主存之间、各种设备之间都通过系统总线交换信息。单总线结构的优点是控制简单方便,扩充方便

@@ 处理机主要由处理器、存储器和总线组成

@@ 总线:数(数据总线) 控(控制总线) 地(地址总线) ISA EISA PCI MCA
SCSI是一种用于计算机和智能设备之间系统级接口的独立处理器标准

@@ 总线复用方式可以减少总线中信号线的数量
总线式一组能为多个部件分时共享的信息传送线,用来连接多个部件并为之提供信息交换通路,通过总线复用方式可以减少总线中信号线的数量,以较少的信号线传输更多的消息

@@ 在计算机系统中采用总线结构,便于实现系统的积木化结构,同时可以减少总线中信号线的数量

@@ 直接主存存取(DMA)[Direct Memory Access]控制方式是在主存与外设之间直接建立数据通路进行数据的交换处理

@@ 在I/O设备与主机间进行数据传输时,CPU只需在开始和结束时作少量处理,而无需干预数据传送过程的是直接存储器存取 [DMA]方式。[直接内存存取(Direct Memory Access)方式的基本思想是通过硬件控制实现主存与**I/O设备**间的直接数据传送,数据的传递过程由DMA控制器进行控制,不需要CPU的干预。在DMA方式下由CPU启动传送过程,即向设备发出”传送一块数据”的命令,在传送过程结束时,DMAC通过中断方式通知CPU进行一些后续处理工作]

@@ 在微机系统中,BIOS(基本输入输出系统)保存在主板上的ROM中 [BIOS是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、开机后自检程序和系统自启动程序,它可从CMOS中读写系统设置的具体信息]

@@ 计算机运行过程中,遇到突发事件,要求CPU暂时停止正在运行的程序,转去为突发事件服务,服务完毕,再自动返回原程序继续执行,这个过程成为中断,其处理过程中保存现场的目的是返回去继续执行原程序

@@ CPU是再一个总线周期结束时响应DMA请求的 [DMA请求的检测点设置在每个机器周期也即总线周期结束时执行,这样使得总线利用率最高]

@@ 计算机中CPU的中断响应时间指的是从发出中断请求到开始进入中断处理程序的时间

@@ 中断向量可提供中断服务程序的入口地址[为了提高响应中断的速度,通常把所有中断服务程序的入口地址[称中断向量]汇聚为中断向量表]

@@ 为了便于实现多级中断嵌套,使用堆栈来保护断点和现场最有效 【实现中断嵌套后进先出的栈来保护断点和现场最有效(当系统中有多个中断请求时,中断系统按优先级进行排队。若在处理低级中断过程中又有高级中断申请中断,则高级中断可以打断低级中断处理,转去处理高级中断,等处理完高级中断后再返回去处理原来的低级中断,称为中断嵌套)

@@ 由I/O设备提出的中断请求是可屏蔽中断,电源掉电是不可屏蔽中断
可将中断分为两大类:
不可屏蔽中断(非屏蔽中断) 和 可屏蔽中断
不可屏蔽中断源[断电]一旦提出请求,CPU必须无条件响应,而对可屏蔽中断源[打印机中断(处于等待请求响应)]的请求,CPU可以响应,也可以不响应。

@@ 计算机在一个指令周期过程中,为从内存读取指令操作码,首先要将**程序计数器(PC)**的内容送到地址总线上
CPU首先从程序计数器获得需要执行的指令地址,从内存(或高速缓存)读取到的指令则暂存在指令寄存器(IR),然后进行分析和执行

程序计数器是PC用于存放下一条指令所在单元的地址的地方
指令寄存器是临时放置从内存里面取得的程序指令的寄存器,用于存放当前从主存储器读出的正在执行的一套指令
地址寄存器用来保存当前CPU所访问的内存单元的地址
指令译码器从内存中取出一条指令经数据总线送往指令寄存器中

@@ 对于只有单条指令的情况下,流水线方式与顺序执行时没有区别的。流水线的原理是在某一时刻可以让多个部件同时处理多条指令,避免各部件等待空闲,由此提高了各部件的利用率,也提高了系统的吞吐率

@@ 流水线的吞吐率是指单位时间流水线处理机输出的结果的数目,因此流水线的吞吐率为一个流水级时间的倒数,即最长流水级时间的倒数

@@ CPU执行算术运算或者逻辑运算时,常将源操作数和结果暂存在累加器(AC)
CPU中常设置多个寄存器,其中,程序计数器的作用是保存待读取 指令在内存中的地址
累加器(AC)是算数逻辑运算单位中用来暂存源操作数计算结果的寄存器指令寄存器
暂存从内存读取的指令,地址寄存器暂存要访问的内存单元的地址。

@@ VLIW(Very Long Instruction Word, 超长指令字) 一种非常长的指令组合,它把许多条指令连在一起,增加了运算的速度

@@ 在机器指令的地址字段中,直接指出操作数本身的寻址方式称为立即寻址[是一种特殊的寻址方式 指令中在操作码字段后面的部分不是通常意义的操作数,而是操作数本身,也就是说数据就包含在指令中,只要取出指令,也就是去除了可以立即使用的操作数]

@@ 若CPU要执行的指令为:MOV R1,#45(即将数值45传递到寄存器R1中),则该指令中采用的寻址方式是寄存器寻址和立即寻址 [45是立即数,R1是寄存器]
立即寻址:操作数就包含在指令中
直接寻址:操作数存放在内存单元中,指令中直接給处操作数所在内存单元的地址
寄存器寻址:操作数存放在某一寄存器中,指令中给出存放操作数的寄存器名
寄存器间接寻址:操作数存放在内存单元中,操作数所在存储单元的地址在某个寄存器中
间接寻址:指令中给出操作数地址的地址
相对寻址:指令地址码给出的是一个偏移量可正可负,操作数地址等于本条指令的地址加上该偏移量
变址寻址:操作数地址等于变址寄存器的内容加偏移量

@@ CISC(Complex Instruction Set Computer)是复杂指令系统计算机的简称 其基本思想是:进一步增强原有指令的功能,用更为复杂的新指令取代原先由软件子程序完成的功能,实现软件功能的硬件化,导致机器的指令系统越来越庞大而复杂。CISC计算机一般所含的指令数目至少300条以上
RISC(Reduced Instruction Set Computer 精简指令集计算机) 其基本思想是:通过减少指令总数和简化指令功能,降低硬件设计的复杂度,使指令能单周期执行,并通过优化编译提高指令的执行速度,采用硬布线控制逻辑优化编译程序。 通常会比CISC配置更多的寄存器、RISC编译器的子程序库通常要比CISC编译器的子程序库大很多、RISC比CISC更加适合VLSI工艺的规整性要求。

@@ Flynn分类法基于信息流特征将计算机分成4类,其中MISD只有理论意义而无实例
Flynn主要根据指令流和数据流来分类:
① 单指令流单数据流机器(SISD)
② 单指令流多数据流机器(SIMD)
③ 多指令流单数据流机器(MISD)[Multi Instruction Single Data] [采用多个指令流来处理单个数据流 在实际情况中,采用多指令流处理多数据流才是最有效的方法,因此MISD只是作为理论模型出现,没有投入实际应用]
④ 多指令流多数据流机器(MIMD)

@@ 取指令时间4Δt,分析时间为2Δt,执行时间为3Δt,按顺序方式从头到尾执行完600条指令所需时间为5400Δt,使用流水线从头到尾执行完600条指令所需的时间为2405Δt [未说流水线就普通乘法计算 流水线则按公式计算]
① 指令顺序执行时,每条指令需要(4Δt+2Δt+3Δt)=9Δt 9Δt×600=5400Δt
② (4Δt+2Δt+3Δt) + (600-1) × 4 = 2405Δt

@@ 指令系统中采用不同寻址方式的目的是扩大寻址控件并提高编程灵活性

@@ 计算机指令一般包括操作码和地址码两部分,为分析执行一条指令,其操作码和地址吗都应存入指令寄存器(IR) [程序被加载到内存后开始运行,当CPU执行一条指令时,先把它从内存器取到缓冲寄存器DR中,再送入IR暂存,指令译码器根据IR的内容产生各种微操作指令,控制其他的组成部件工作,完成所有功能。]

@@ CPU产生每条指令的操作信号并将操作信号送往相应的部件进行控制、CPU中的控制器决定计算机运行过程的自动化、指令译码器是CPU控制器中的部件
CPU是计算机的控制中心,主要由运算器、控制器、寄存器组、和内部总线等部件组成。
控制器由程序计数器、指令寄存器、指令译码器、时序产生器和操作控制器组成

@@ 可靠度串并联:串联系统可靠度为R1×R2,并联系统可靠度为R1/R2=1-(1-R)

@@ 计算机系统的可靠性可以用**MTBF / (1+MTBF)**来度量,其中MTBF为平衡失效间隔时间 [又称平均无敌故障时间] 是衡量一个产品(尤其是电器产品)的可靠性指标,单位为”小时”

@@ 软件产品的可靠性并不取决于软件产品的开发方式 而取决于潜在的错误数量和位置,软件产品的使用方式;软件可靠性指的是一个系统对于给定的时间间隔内,在给定条件下**无失效运作的概率**。

@@ 软件可靠性是指一个系统在给定时间间隔内和给定条件下无失败运行的概率,一个软件系统能够按照规格说明正确运行的概率。软件可维护性是在给定的使用条件下,在规定的时间间隔内,使用规定的过程和资源完成维护活动的概率

@@ 应用级网关防火墙是内部网和外部网的隔离点,它可对应用层的通信数据流进行监控和过滤。应用级网关可以工作在OSI七层模型上的任意一层,能够检查进出的数据包,通过网关复制传递数据,防止在受信任服务器和客户机与不受信任的主机之间直接建立联系。

@@ 用户A和B要进行安全通信,通信过程需确认双方身份和消息不可否认,A和B通信时使用数字证书来对用户的身份进行认证;使用数字签名确保消息不可否认。数字签名是对真实性的保护

@@ 震网病毒是一种破坏工业基础设施的恶意代码,利用系统漏洞攻击工业控制系统,是一种危害性极大的蠕虫病毒蠕虫(Worm)是一个程序或程序序列。它利用网络进行复制和传播,传染途径是通过网络、移动存储设备和电子邮件。最初的蠕虫病毒定义是在DOS环境下,病毒发作时会在频幕上出现一条类似虫子的东西,胡乱吞吃屏幕上的字母并将其改正。常见的蠕虫病毒有:快乐时光、红色代码、爱虫病毒、熊猫烧香、Nimda病毒、爱丽兹病毒CIH:系统病毒 X卧底:以木马形式传播目标为智能手机的病毒
引导区病毒:破坏的是引导盘、文件目录
宏病毒[Macro]:破坏的是OFFICE文件相关(病毒宏将自身复制至Word的通用Normal模板中)
木马:一般强调控制操控 [冰河是木马软件,主要用于远程监控。冰河木马后经其他人多次改写形成多种变种,并被用于入侵其他用户的计算机木马程序]

@@ Sniffer(嗅探器)是一种基于被动侦听原理的网络分析方式。使用这种技术方式可以监视网络的状态、数据流动情况以及网上传输的信息。它不是木马程序

@@ 属于DoS攻击的是SYN Flooding攻击 [DoS是Denial of Service的简称,即拒绝服务,造成DoS的攻击行为被称为DoS攻击,其目的是使计算机或网络无法提供正常的服务。最常见的DoS攻击有计算机网络宽带攻击和连通性攻击。]
特洛伊木马[Trojan]是附着在应用程序中或者单独存在的一些恶意程序,它可以利用网络远程控制网络的另一端的安装有服务端服务的主机,实现对被植入了木马程序的计算机的控制,或者窃取被植入了木马程序的计算机上的机密资料。
拒绝服务攻击通过网络的内外部用户来发动攻击。内部用户可以通过长时间占用系统的内存、CPU处理时间使其他用户不能及时得到这些资源,而引起拒绝服务器攻击;外部黑客也可以通过占用网络连接使其他用户得不到网络服务 SYN Flooding攻击以多个随机的源主机地址向目的路由器发送SYN包,在收到目的路由器的SYN ACK后并不回应,于是目的路由器就为这些源主机建立大量的连接队列,由于没有收到ACK一直维护着这些队列,造成了资源的大量消耗而不能向正常请求提供服务,甚至导致服务器崩溃。服务器要等待超时才能断开已分配的资源,所以SYN Flooding攻击是一种DOS攻击
端口欺骗攻击是采用端口扫描到系统漏洞从而实施攻击
IP欺骗攻击是产生的IP数据包为伪造的源IP地址,以便冒充其他系统或发件人的身份

@@ DES是共享密钥加密算法;常见的对称加密算法有:DES、三重DES、RC-5、IDEA、AES
共享密钥加密对称加密
非共享密钥加密指公开密钥加密

@@ 计算机病毒的特征:隐蔽性、传染性、潜伏性、触发性、破坏性

@@ MD5是摘要算法,对任意长度的输入计算得到的结果长度是128未散列值

@@ 攻击者通过发送一个目的主机已经接受过的报文来达到攻击目的,这种攻击方式属于重放攻击
**重放攻击(Replay Attacks)**又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行

@@ kerberos系统中可通过在报文中加入时间戳来防止重放攻击。发送的数据包带时间戳的,服务器可以根据时间戳来判断是否为重放包,以此防止重放攻击

@@ 防火墙最基本的功能就是控制在计算机网络中,不同信任程度区域间传送的数据流。防火墙对流经它的网络通信进行扫描,这样能够过滤掉一些攻击,以免其在目标计算机上被执行。防火墙还可也关闭不适用的端口,屏蔽内部细节。所有的访问都经过防火墙,防火墙就能记录下这些访问并做出日志记录,同时也能提供网络使用情况的统计数据

@@ 防火墙的性能及特点:
工作层次:决定防火墙效率及安全的主要因素,工作层次越低,则工作效率越高,其安全性就越低
防火墙机制:采用代理机制,则防火墙具有内部信息隐藏的特点,相对而言,安全性越高,效率越低。如果采用过滤机制,则效率高,安全性却降低

@@ 包过滤防火墙和代理服务防火墙的叙述中正确的是包过滤技术对应用和用户是透明的。包过滤技术是一种基于网络层、传输层的安全技术,优点是简单实用,实现成本较低同时,包过滤操作对于应用层来说是透明的,使用此类防火墙时外部网络与内部网络之间不存在直接连接,即使防火墙发生了问题,外部网络也无法被保护的网络连接。

@@ 包过滤防火墙对数据包的过滤依据包括源IP地址、源端口号、目标IP地址、目标端口号 而不是MAC地址

@@ 防火墙通常分为内网、外网、DMZ三个区域
按照受保护程序[安全级别]从高到低正确排列顺序依次为内网、DMZ、外网
① 内网可以访问外网、内网可以访问DMZ
② 外网可以访问DMZ、外网不能访问内网
③ DMZ不能访问外网、DMZ不能访问内网

@@ 入侵检测技术包括专家系统、模型检测、简单匹配 [入侵检测流量控制属于网络安全措施]

@@ 这四种加密算法均能对明文进行加密。
RSA是一种非对称加密算法,由于加密和解密的密钥不同,因此便于密钥管理和发放,同时用户或机构之间进行身份认证方面有较好的应用 [基于大数定律,通常用于对消息摘要进行签名(数字签名算法) 而IDEA和RC4适用于对数据传输加密]
SHA-1是一种安全散列算法,常用于对接接收到的明文输入产生固定长度的输出,来确保明文在传输过程中不会被篡改
MD5是一种使用最为广泛的报文摘要算法(摘要算法)
RC5是一种用于对明文进行加密的算法,在加密速度和强度上均较为合适,适用于大量明文进行加密并传输

@@ 假定用户A、B分别为I1和I2两个CA处取得了各自的证书,下面I1、I2互换公钥是A、B互信的必要条件

@@ MIME是一个互联网标准,扩展了电子邮件标准,使其能够支持,与安全无关。
SSLHTTPS涉及到邮件传输过程的安全
PGP是一套用于信息加密、验证的应用程序,可用于加密电子邮件内容

@@ 数字签名用于通信的A、B双方,使得A向B发送签名的消息P,提供以下服务:
① B可以验证消息P确实是来源于A
② A不能否认发送过消息P
③ B不能便在或修改消息P
数字签名首先需要生成消息摘要,使用非对称加密算法以及私钥对摘要进行加密。接收方使用发送方的公钥对消息摘要进行验证。

@@ 用户B收到用户A带数字签名的消息M,为了验证M的真实性,首先需要从CA获取用户A的数字证书,并利用CA的公钥验证该证书的真伪,然后利用A的公钥验证M的真实性 [验证证书的真伪需要用CA的公钥验证CA的签名,验证M的真实性需要用用户A的公钥验证用户A的签名]

@@ 从认证中心CA获取用户B的数字证书,该证书用CA的私钥数字签名从用户B的数字证书中可以获得B的公钥CA公钥→合法性、CA签名→真伪、CA私钥→数字签名

@@ 公钥体系中,私钥用于解密和签名,公钥用于加密和认证

@@ 某网站向CA申请了数字证书,用户通过CA的签名来验证网站的真伪,通过使用CA的公钥来确定该网站的合法性

@@ 设置防雷击属于物理线路安全措施、入侵检测, 流量控制属于网络安全措施、漏洞发现于补丁管理属于系统安全措施

@@ 网络攻击主动攻击被动攻击两类,主动攻击是指通过一系列方法主动向被攻击对象试试破坏的一种攻击方式,主动攻击有:重放攻击、IP地址欺骗、拒绝服务流量分析攻击[被动攻击]是通过持续检测现有网络中的流量变化或者变化趋势,而得到相应信息的一种被动攻击方式

@@ 端口443明确用于HTTPS服务,因此是HTTPS(加密)流量的标准端口。 它也称为HTTPS端口443,因此所有受保护的事务都是使用端口443进行的。 您可能会惊讶地发现,几乎95%的安全站点都使用端口443进行安全传输。80端口是HTTP超文本传输协议

Proto Local Address Foreign Address State
TCP 192.168.0.200:2011 202.100.112.12:443 ESTABLISHED
TCP 192.168.0.200:2052 128.105.129.30:80 ESTABLISHED
TCP 192.168.0.200:2038 100.29.200.110:110 TIME_WAIT

① 其中已经与主机202.100.112.12的443端口建立连接,由于443端口主要用于HTTPS服务,是提供加密和通过安全端口传输的另一种HTTP协议,是建立了安全连接
② 其中已经与主机128.105.129.30的80端口建立了普通连接。
③ 其中与主机100.29.200.110的110端口正在等待建立连接

@@ SSH(Secure Shell)是终端设备远程站点之间建立安全连接的协议,是专为远程登录会话和其他网络服务提供安全性的协议,利用SSH协议可以有效防止远程管理过程中的信息泄露问题,SSH最初是UNIX上的程序

@@ 机房安全属于物理安全、入侵检测属于网络安全、漏洞补丁管理属于系统安全、数据库安全属于应用安全

@@ 网络系统中,通常把Web服务器置于DMZ区 [DMZ是为了解决安装防火墙后外部网络不能访问内部网络服务器的问题,而设立的一个非安全系统与安全系统之间的缓冲区。这个缓冲区位于企业内部网络和外部网络之间的小网络区域内,在这个小网络区域内可以防止一些必须公开的服务器设施,如企业Web服务器、FTP服务器和论坛]

@@ 拒绝服务攻击即攻击者想办法让目标机器停止提供服务,这是黑客常用的攻击手段之一,其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分,只要能够对目标造成麻烦,使某些服务被暂停甚至主机死机,都属于拒绝服务攻击。攻击者进行拒绝服务攻击,实际上让服务器实现两种效果:一是迫使服务器的缓冲区满,不接收新的请求。二是使用IP欺骗,迫使服务器把合法用户的连接复位,影响合法用户的连接

@@ PKI体制中,保证数字证书不被篡改的方法是用CA的私钥对数字证书签名[防伪造,不可抵赖]

@@ ECC、DSA、RSA属于公开密钥加密算法 DES属于私钥加密体制

@@ 利用报文摘要算法生成报文主要的目的是防止发送的报文被篡改 [报文摘要是用来保证数据完整性的,传输的数据一旦被修改,摘要就不同了,只要对比两次摘要就可以确定数据是否被修改过]

@@ TLS是安全传输层协议的简称,用于在两个通信应用程序之间提供保密性和数据完整性
SSL是安全套接层协议的简称,是一种为网络通信提供安全和数据完整性的协议,它与TLS非常相似,它们都是在传输层对网络连接进行加密
PGP是基于RSA公钥加密体系的邮件加密软件,用它可以对邮件保密以防止非授权者阅读
HTTPS即安全版的HTTP(超文本传输协议),它是在HTTP下加入SSL层,HTTPS的安全基础就是SSL
IPSec网络层的安全协议,它通过使用加密的安全服务来确保在网络上进行保密而安全的通讯

@@ 在Windows系统中,默认权限最低的用户组是everyone
用户组默认权限由高到低的顺序是administrators > power users > users > everyone

@@ IIS6.0支持的身份验证安全机制有[.NET Passport身份验证、集成Windows身份验证、摘要式身份验证、基本身份验证]安全级别最高的验证方法是集成Windows身份验证

@@ 利用漏洞扫描系统可以获取某FTP服务器中是否存在可写目录的信息 [通过与目标主机TCP/IP端口建立连接并请求某些服务(TELNET、FTP),记录目标主机的应答,搜索目标主机相关信息,从而发现目标主机某些内在的安全弱点]

@@ FTP服务器的控制端口为21,上传文件时的端口为20

@@ 防范网络监听最有效的方法是数据加密

@@ “三网合一”的三网是指电信网、广播电视网、互联网

程序语言

@@ LISP是一种通用高级计算机程序语言、C是命令式过程式语言、Java面向对象语言、C#结构化程序设计语言、Java,Python,JavaScript都是解释型程序设计语言[其中python[弱类型语言]和javascript是脚本语言(主要采用解释方式实现)]
强/弱类型指的是语言类型系统的类型检查的严格程度。弱类型相对于强类型来说类型检查更不严格, 比如允许变量类型的隐式转换,允许强制类型转换

@@ 语法制导翻译是一种静态语义分析方法。程序设计语言的语义分为静态语义动态语义,其中静态语义分析方法是语法制导翻译,其基本思想是将语言结构的语义以属性的形式赋予代表此结构的文法符号,而属性的计算以语义规则的形式赋予文法的产生式。

@@ 常用的函数参数传递方式有传值传引用两种;在传引用方式下,修改形参实质上改变了实参的值。[传值调用方式下,是将实参的值传递给形参,该传递是单方向的,结束调用后不会再将形参的值传递給实参引用调用方式下,实质上是将实参的地址传递給形参,借助指针的间接访问数据方式下(或者将形参看作是实参的别名),在被调用函数中对形参的修改实质上是对实参的修改。]

@@ 在值调用方式下是将参数的值传給形参。在引用调用方式下是将实参的地址传递给形参

@@ 在传值调用方式下,实参可以是变量,也可以是常数和表达式
引用调用方式下,可以是实现形参和实参间双向传递数据的效果

@@ 传地址方式下,实参的地址传給形参,因此,实参必须有地址

@@ 函数中print(x)执行后输出的值为40

main(){                      f(int x, int &a){
  int x = 5;                    x = x * x - 1;
  f(x+1,x);                        a = x + a;
  print(x);                        return;
}                               }

@@ 函数中return(a-x)执行后输出的值为33

@@ 编译器对高级语言源程序的处理过程:
源程序 → 词法分析 → 语法分析 → 语义分析 → 中间代码生成 → 代码优化 → 目标代码生成 → 目标代码
其中中间代码生成和代码优化并不是每个编译器都必需的,与编译器相比,解释器参与运行控制,程序执行的速度慢

词法分析:根据语言的词法规则,对源程序进行逐个字符地描述,从中识别出一个个单词符号,针对词汇的检查
语法分析:在词法分析的基础上,根据语言的语法规则将单词符号序列分解成各类语法单位;
语义分析:分析各语法结构的含义,检查源程序是否包含语义错误,主要针对句子含义的检查;

@@ 语法分析阶段的输入是记号流,若程序中的符号不匹配,则会在语法分析阶段检查出错误
记号流:词法分析的输出是记号流,也就是语法分析的输入。
源程序:词法分析的任务是把源程序的字符串转换成单词符号序列
分析树:如果没有语法错误,语法分析后就能正常的构造出语法树

@@ 语法错误是指语言结构上的使用错误,是指编译时所发现的程序错误,如单词拼写错误、标点符号错误、表达式中缺少操作数、符号不匹配等有关语言结构上的错误 [语法分析阶段可以发现程序中所有的语法错误、但是不能在语义分析阶段发现程序中的所有语义错误(循环越界)]

@@ 若程序在运行时陷入死循环,则情况属于动态的语义错误;在编译过程中,进行类型分析和检查是语义分析阶段的一个主要工作。**死循环,零除数,其他逻辑错误是属于动态语义错误。语义检查时不检查动态语义,因此编译正确的程序不包含语法错误**

@@ 高级程序设计语言不依赖具体的机器硬件;程序中局部变量的值在运行时可以改变,局部函数只能在函数内部使用,其作用域是从定义位置起至函数体或复合语句体结束位置。

@@ 在编写C/C++时,若表达式中引用的变量从定义到使用始终没有赋值,则该变量中的值表现为一个随机数,这样对表达式的求值结果就是不确定的了。故[可以通过编译并运行,但运行结果不一定是期望的结果]

@@ 关于高级程序设计语言翻译的叙述中正确的是目标代码生成阶段的工作与目标机器的体系结构密切相关

@@ 变量与常量:变量具有类型属性,常量则没有 [常量在程序运行过程中不能修改]

@@ 可视化程序设计是以”所见即所得“的编程思想为原创,力图实现编程工作的可视化,即随时可以看到结果,程序与结果的调整同步。可视化程序设计仅通过直观的操作方式即可完成界面的设计工作[其基于面向对象的思想,引入了控件的概念和事件驱动;程序开发遵循步骤:即先进行界面的绘制工作,再基于事件编写程序代码,以响应鼠标、键盘的各种动作]。可视化程序设计最大的有点是设计人员可以不用编写或只需要编写很少程序代码,就能完成应用程序的设计,极大提高设计人员的工作效率

@@ PHP不是标记语言,PHP,全称 Hypertext Preprocessor ,中文翻译“超文本预处理器”。是在 服务器 端执行的 脚本语言 ,尤其适用于 Web 开发并可嵌入 HTML 中。

@@ 创建指向邮箱地址的连接**<a href = "mailto:test@test.com"> test@test.com </a>** 使用**<mailto>**标签定义一个指向电子邮件地址的超级链接

@@ 指针变量可以是全局变量也可以是局部变量

@@ 若一个程序语言可以提供链表的定义和运算,则其运算时的数据空间必须采用堆存储分配策略

@@ 动态语言是指程序在运行时可以改变其结构,例如新的函数可以被引进、已有的函数可以被删除等,在结构上的变化。动态语言的类型检查是在运行时进行的。优点是方便阅读,不需要写非常多的与类型相关的代码;缺点是不方便调试,命名不规范时会读不懂、不利于理解。事实上脚本语言都是动态语言,而动态语言都是解释型语言,不管它们是否是面向对象的语言 ;C语言属于静态语言,其所有成分可在编译时确定

@@ HTML<body>元素中,vlink属性用于定义超链接被鼠标点击后所显示的颜色
alink:用于设置正在被击中的链接的颜色
vlink:用于设置已使用的链接的颜色
bgcolor:用于设置文档整体背景颜色
background:用于设置背景图片的URL

@@ XML文档语法规范中:标记之间不可以交叉嵌套

@@ 动态绑定是指在运行时把过程调用和响应调用所需要执行的代码加以结合。运行时结合动态绑定编译时结合静态绑定

@@ 汇编程序的功能是将用汇编语言写的源程序翻译成机器指令程序。汇编程序的基本工作包括将一条可执行汇编语句转换成对应的机器指令;处理源程序中出现的伪指令。由于汇编指令中形成操作数指令的部分可能出现后面才会定义的符号,所以汇编程序一般需要两次扫描源程序才能完成翻译过程

@@ 对高级语言源程序进行编译或解释处理的过程中,需要不断收集、记录和使用源程序中一些相关符号的类型和特征等信息,并将其存入符号表 [这些信息一般以表格形式存储于系统中]

@@ 将高级语言源程序翻译称目标程序的是编译程序

@@ 将高级语言源程序翻译为可在计算机上执行的形式有多种不同的方式,其中编译方式生成逻辑上与源程序等价的目标程序,解释方式不生成[为源程序中变量所分配的存储单元的地址属于逻辑地址]
编译语言是一种以编译器来实现的编程语言,它不像直译语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序,称为编译程序

@@ 将高级语言源程序通过编译或解释方式进行翻译时,可以先生成与源程序等价的某种中间代码。后缀式和三地址码是常用的中间代码。[不同高级程序语言可以产生同一种中间代码]
中间代码的作用是可使程序的结构再逻辑上更为简单明确(有利于进行与机器无关的优化处理,优化一般建立在对程序的控制流和数据流分析的基础之上, 与机器无关),特别是可以使目标代码的优化比较容易实现。中间代码由很多形式,常见的由**逆波兰记号(后缀式)、四元式、三元式(三地址码)、语法树,它们的共同特点是与具体的机器无关,不依赖于具体的计算机。中间代码**是源程序的一种内部表示,称中间语言。中间代码不依赖于具体的机器、使用中间代码可提高编译程序的可移植性、中间代码可以用树或图表示

@@ 编译程序不参加与用户程序的运行控制,而解释程序则参与。编译程序(编译器)则是将源程序翻译成目标语言程序,然后再计算机上运行目的程序。再解释方式下。翻译源程序时不生成独立的目标程序,而编译器则将源程序翻译成独立保存的目标程序。

@@ 编译和解释是实现高级程序设计语言翻译的两种基本形式,在编译方式下,必须进行词法、语法和语义分析,然后再产生源程序的目标代码

@@ 对高级语言源程序进行编译的过程可分为多个阶段,分配寄存器的工作再目标代码生成阶段进行。[目标代码生成是编译器工作的最后一个阶段,这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码、了重定位的指令代码或汇编指令代码,这个阶段的工作与具体的机器密切相关,因此在目标代码生成阶段分配寄存器。]

@@ 汇编程序输入的是用汇编语法书写的源程序,输出的是用机器语言表示的目标程序[汇编语言源程序中的指令语句将被翻译成机器代码汇编程序以汇编语言源程序为输入,以机器语言表示的目标程序为输出汇编语言的指令语句必须具有操作码字段,可以没有操作数字段]

@@ 编译过程中,对高级语言程序语句的翻译主要考虑声明语句和可执行语句。对声明语句,主要是将所需要的信息正确地填入合理组织的符号表中;对可执行语句,则是翻译成中间代码或目标代码

@@ 编译解释是语言处理的两种基本方式:
编译过程包括(词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成等阶段),以及**符号表管理出错处理模块**
解释过程在词法、语法和语义分析方面与编译程序的工作原理基本相同,但是在运行用户程序时,它直接执行源程序或源程序的内部形式
这两种语言处理程序的根本区别:在编译方式下,机器上运行的是与源程序等价的目标程序,源程序和编译程序都不再参与目标程序的执行过程;而在解释方式下,解释程序和源程序(某种等价表示)要参与到程序的运行过程中,运行程序的控制权在解释程序。解释器翻译源程序时不产生独立的目标程序,而编译器则需要将源程序翻译成独立的目标程序

@@ 移进—归约分析法是编译程序(解释程序)对高级语言源程序进行语法分析的一种方法,属于自下而上的语法分析方法
递归下降分析法、预测分析法 → 是自上而下分析法
移进-归约分析法 → 是**自下而上**分析法

@@ 已知某高级语言源程序A经编译后得到机器C上的目标程序B,则对B进行反编译,不能还原出源程序A
编译是将高级语言源程序翻译成机器语言程序,反编译是编译的逆过程反编译通常不能把可执行文件还原成高级语言源程序,只能转换成功能上等价的汇编程序

@@ 对于表达式a or ((c<d) and b),当a为true时可进行短路计算,最后计算的是or,对 或运算 ,只要有一个真则结果为真,此题当a为true时,可进行短路计算,直接得到后面的结果

@@ 文法分为4种类型,程序设计语言的大多数语法现象可用乔姆斯基上下文无关文法描述

@@ 在仅由字符a、b构成的所有字符串种,其中以b结尾的字符串集合可用正则表达式为
$(b|ab)^b$表示的字符串集合{b,bb,abb,bbb,abab,bbbb,abbb,babb,...}除了以b结尾, 还要求每个a后面至少有1个b 不符合题意只要有b结尾的!!
$(a|b)^
b$ 表示的字符串集合{b,ab,bb,aab,abb,bab,bbb,aaab,aabb,abab,abbb,baab,babb,bbab,...}

@@ 表达式采用逆波兰式表示时,利用进行求值
后缀式(逆波兰式)表达方式把运算符写在运算对象后面,**把a+b写成ab+**,所以也称为后缀式。借助栈可以方便地对后缀式进行求职。

@@ 与算术表达式**(a+(b-c))*d**对应的树是 求值运算处理顺序是:先进行b-c,然后与a相加,最后再与d相乘[中序遍历二叉树即可得出]

@@ 算数表达式**(a-b)(c+d)的后缀式是ab-cd+** 后缀式(逆波兰式)

@@ 算术表达式*a+(b-C)*d**的后缀式是abc-d+** 利用逆波兰式,但是要把得到的步骤式子当成整体
原式=a+bC-*d =a+bC-d*=abC-d*+ 其中的着重点在于把bC-变成一个整体bC- * d再用逆波兰式 bC-d*
算术表达式**(a-b)c+d的后缀式是ab-cd+**

@@ 逻辑表达式"a^bvc^(bvx>0)" 的后缀式是**ab^cbx0>v^v**
此题有大坑 ^是"与"门的意思、v是异"或"门的意思
逻辑与运算的优先级高于逻辑或运算
逻辑与运算“ 表达式 “x^y“的短路求值逻辑是:若x为假,则可知”x^y“的值为假,无需再对y求值。因此只有在x为真时继续对y求值
逻辑或运算“ 表达式 “xvy“的短路求值逻辑是:若x为真,则可知”xvy“的值为真,无需再对y求值。因此只有在x为假时继续对y求值
对于逻辑表达式"a^bvc^(bvx>0)",从运算符的优先级方面考虑需先对”a^b“求值。然后对”c^(b^x>0)“求值,最后进行对"v"运算,因此后缀式是**ab^cbx0>v^v**

@@ 递归下降分析方法是一种自上而下的语法分析
程序式设计语言的绝大多数语法规则可以采用上下文无关文法进行描述。语法分析方法有多种,根据产生语法树的方向,可分为自底向上[算符优先分析法、LR分析法] 自顶向下[递归下降分析法、预测分析法]两类。

@@ 对于大多数通用程序设计语言,用上下文无关文法描述其语法即可
文法分成四种类型:0型、1型、2型、3型。
0型文法也称为短语文法,其能力相当于图灵机,任何0型语言都是递归可枚举的;反之,递归可枚举集也必定是一个0型语言
1型文法也称为上下文有关文法,这种文法意味着对非终结符的替换必须考虑上下文
2型文法也称为上下文无关文法,非终结符的替换无需考虑上下文【通用程序设计语言绝大多数使用上下文无关文法(此文法拥有足够强的表达能力来表示大多数程序设计语言的语法)】
3型文法等价于正规式,因此也被称为正规文法或线性文法。

@@ 在c/c++程序种,整型变量a的值为0且应用在表达式”c=b/a”种,则最可能发生的情形是运行时产生异常
c=b/a符合c/c++语言的语法逻辑,编译时不会报错,运行时,代入a的值,发生错误

@@ 对高级语言源程序进行编译过程中,有限自动机(NFA或DFA)是进行词法分析的适当工具
词法分析的任务是把构成源程序的字符串转换成单词符号序列。有限自动机是一种识别装置的抽象概念,它能准确地识别正规集。有限自动机分为两类:确定的有限自动机(DFA)不确定的有限自动机(NFA)

@@ L={$a^n$$b^n$|n>=1}不能用正规式表示,也不能通过有限自动机识别[此字符串的特点是a的个数与b的个数相同,且所有的a都在b之前],因为该集合不是正规集,不能用正规式表示。而语言**L={$a^m$$b^n$|m≥0, n≥1}**的正规式表达式式a*bb* 因为在此集合中表示,若干个a之后跟若干个b,a可以不出现,b至少出现1次。

@@ 包含8个成员的开发小组的沟通路径最多有28条 [任意小组成员之间均可能有沟通路径,则可用完全连通图来对开发小组的沟通路径建模,最多的沟通路径为完全连通图的边数,即n个成员的开发小组的沟通路径是n(n-1)/2,因此8个成员的开发小组沟通路径最多有28条]

@@ 一个文法的语言是从文法能产生的句子的集合。一个文法产生的句子是从文法开始符号发出推导出的所有终结符号串

操作系统

@@ 从减少成本和缩短研发周期考虑,要求嵌入式操作系统能运行在不同的微处理器平台上,能针对硬件变化进行结构与功能上的配置。该要求体现了嵌入式操作系统的可定制性
嵌入式操作系统的特点:
微型化:从性能和成本角度考虑,希望占用的资源和系统代码量少
可定制:从减少成本和缩短研发周期考虑,要求嵌入式操作系统能运行在不同的微处理器平台上,能针对硬件变化进行结构与功能上的配置,以满足不同应用的需求
实时性:嵌入式操作性系统主要应用于过程控制、数据采集、传输通信、多媒体信息及关键要害领域需要迅速响应的场合,所以对实时性要求较高
可靠性:系统构建、模块和体系结构必须达到应有的可靠性,对关键要害应用还要提供容错和防故障措施
可移植性:为了提高系统的易移植性,通常采用硬件抽象层板级支撑包的底层设计技术

@@ 在Linux中,要更改一个文件的权限设置可使用chmod命令,修改文件夹名可使用modify命令
Linux中只有一个根目录,用”/“表示

@@ 计算机系统由硬件软件两部分组成,通常把未配置软件的计算机称为裸机。操作系统目的是为了填充人与机器之间的鸿沟,即建立用户与计算机之间的接口,而为裸机配置的一种系统软件。从用户角度看,当计算机配置了操作系统后,用户不再直接使用计算机系统硬件,而是使用操作系统所提供的命令和服务去操纵计算机,操作系统已称为现代计算机系统中必不可少的最重要的系统软件,因此操作系统用户计算机之间接口

@@ 实时操作系统主要用于实时要求的过程控制等领域,实时系统对于来自外部的事件必须在被操控对象规定的事件内做出及时响应并对其进行处理
实时操作系统分时操作系统的第一点区别就是:
① 交互性强弱不同,分时系统交互型强,实时系统交互型弱,但可靠性要求高
② 响应事件的敏感性强,对随机发生的外部事件必须在被控制对象规定的时间做出及时响应并对其进行处理
③ 系统的设计目的不同,分时系统是设计称一个多用方的通用系统,交互能力强;而实时系统大多数都是专用系统。

@@ 嵌入式系统初始化过程分为3个主要环节,按照自底向上从硬件到软件的次序依次为:片级初始化板级初始化系统级初始化。系统初始化主要任务是以软件初始化为主,主要进行操作系统的初始化
片级初始化:片级初始化完成嵌入式微处理器的初始化,包括设置嵌入式微处理器的核心寄存器和控制寄存器、嵌入式微处理器核心工作模式和嵌入式微处理器的局部总线模式等。片级初始化把嵌入式微处理器从上电时的默认状态逐步设置成系统所要求的工作状态。这是一个纯硬件的初始化过程。
板级初始化:板级初始化完成嵌入式微处理器以外的其他硬件设备的初始化。另外,还需设置某些软件的数据结构和参数,为随后的系统级初始化和应用程序的运行建立硬件和软件环境。这是一个同时包含软硬件两部分在内的初始化过程。
系统初始化:系统初始化过程以软件初始化为主,主要进行操作系统的初始化。BSP将对嵌入式微处理器的控制权转交给嵌入式操作系统,由操作系统完成余下的初始化操作,包含加载和初始化与硬件无关的设备驱动程序,建立系统内存区,加载并初始化其他系统软件模块,如网络系统、文件系统等。最后,操作系统创建应用程序环境,并将控制权交给应用程序的入口。

@@ 设计操作系统时不需要考虑的问题是语言编译器的设计实现
操作系统有两个重要作用:
① 通过资源管理提高计算机系统的效率,操作系统是计算机的资源管理者,它含有对系统软/硬件资源实施管理的一组程序
② 改善人机界面,向用户提供有好的工作环境

@@ 磁盘格式化是指把一张空白的盘划分称一个个小区域并编号,以供计算机存储和读取数据,格式化是一种纯物理操作,实在磁盘的所有数据区上写0的操作过程,同时对硬盘介质做一致性检测,并且标记出不可读和坏的扇区。由于大部分硬盘在出厂时已经格式化过,所以有在硬盘介质产生错误时才需要进行格式化。
磁盘分区是将磁盘划分成一块块的存储区域。在传统的磁盘管理中,将一个硬盘分为两大类分区:主分区和扩展分区。主分区是能够安装操作系统、能够进行计算机启动的分区,这样的分区可以直接格式化,然后安装系统,直接存放文件。
磁盘里的文件都是按存储时间先后来排列的,理论上文件之间都是紧凑排列而没有空隙的。但是,用户常常会对文件进行修改,而且新增加的内容并不是直接加到原文件的位置的,而是放在磁盘存储空间的最末尾,系统会在这两段之间加上联系标识。当有多个文件被修改后,磁盘里就会有很多不连续的文件。一旦文件被删除,所占用的不连 续空间就会空着,并不会被自动填满,而且,新保存的文件也不会放在这些地方,这些 空着的磁盘空间,就被称作”磁盘碎片”。因此,硬盘的每个分区里都会有碎片。碎片太多,其他的不连续文件相应也多,系统在执行文件操作时就会因反复寻找联系标识,工作效率大大降低,直接的反映就是感觉慢。磁盘清理将删除计算机上所有不需要的文件(这些文件由用户或系统进行确认)。
磁盘碎片整理就是通过系统软件或者专业的磁盘碎片整理软件对电脑磁盘在长期使用过程中产生的碎片和凌乱文件重新整理,释放出更多的磁盘空间,可提高电脑的整体性能和运行速度。

@@ 在WinXP操作系统中,用户利用”磁盘管理“程序可以对磁盘进行初始化、创建卷,可以选择使用FAT、FAT32或NTFS文件系统格式化券,通常将"C:\Windows\myprogram.exe"文件设置成只读隐藏属性,以便控制用户对该文件的访问。这一级安全管理称之为文件级安全管理。
文件级安全管理是通过系统管理员或文件组对文件设置来控制用户对文件的访问。用户对文件的访问,将由用户访问权、目录访问权、文件属性三者的权限所确定。

@@ 假设磁盘臂位于15号柱面上,进程的请求序列如下表表示,如果采用最短移臂调度算法,那么系统的响应序列应为⑤①②④③⑥

请求序列 柱面号 磁头号 扇区号
12 8 9
19 6 5
23 9 6
19 10 5
12 8 4
28 3 10

最短移臂调度算法,即有限响应距离比较近磁道的申请
1.当磁头位于15号柱面 (柱面号即磁道编号),请求序列分别位于12号柱面(①⑤)、19号柱面(②④)、23号柱面(③)、28号柱面(⑥)
2.距离15号柱面最近的应该是12号柱面(①⑤),优先响应(①⑤),次序不限;

@@ P(Pass)(通过)消耗一个信号量 V(释放)发送一个信号量
V(S1)唤醒P(S1):收银员进程操作只有等待着购书者拿出去前台后才能触发

@@ PV操作是操作系统提供的具有特定功能的原语,利用PV操作可以实现资源的互斥使用
软考必考题型之PV操作_pv操作中p和v各代表什么-CSDN博客

@@ 某计算机系统中互斥资源R的可用数为8,系统中有3个进程P1、P2和P3竞争R,且每个进程都需要i个R,该系统可能会发生死锁的最小i值为:4
本题对于R资源可用数为8,分配到3个进程中,为了让最后的i值最小,所以每个进程尽量平均分配,可以得到3、3、2的分配情况,此时如果假设i的取值为3,则必定不会发生死锁,当i>3时系统会产生死锁,此时取整,即最小i值为4

@@ 常用的进程控制块的组织方式有链接方式索引方式
采用链接方式是把具有同一状态的PCB,用其中的链接字链接成一个队列。这样可以形成就绪队列、若干个阻塞队列空白队列等。其中的就绪队列常按进程优先级的高低排列,把优先级高的进程的PCB排在队列前面。此外,也可根据阻塞原因的不同而把处于阻塞状态的进程的PCB排成等待I/O操作完成的队列和等待分配内存的队列等。
采用索引方式是系统根据所有进程的状态建立若干索引表。例如,就绪索引表、阻塞索引表,并把各索引表在内存的首地址记录在内存的一些专用单元中。在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址。

@@ 某系统中有3个并发进程竞争资源R,每个进程都需要5个R,那么至少有13个R,才能保证系统不会发生死锁。[每个进程所需资源数-1,再相加,就是发生死锁的临界资源数]
3(5-1)+1<=y 所以y=13个

某系统有n个进程,每个进程需要x个同类资源,系统不会产生死锁的最少资源是多少?
★ 设:最少资源数位y个,满足如下表达式即可:★
n(x-1)+1<=y
每个进程需要x个资源,初始时給每个进程x-1个资源,最后再給一个格外的资源,每当一个进程运行完后马上释放其占有的资源,其它进程可以申请资源继续运行

@@ 某系统中仅有5个并发进程竞争某类资源,且都需要3个该类资源,那么至少有11个该类资源,才能保证系统不会发生死锁。5×(3-1)+1=11

@@ 某计算机系统页面大小为4k,进程的页面变换表如下。若进程的逻辑地址为2D16H。该地址经过变换后,其物理地址应为4D16H

页号 物理块号
0 1
1 3
2 4
3 6

根据题意,页面大小为4k,逻辑地址2D16H所在页号为2,页内地址为D16H,查看表后可知物理块号为4,该地址i过变换后,其物理地址应为物理块号4拼接上页内地址D16H,即十六进制4D16H

@@ 让我们来看一下这道13年上半年的软设选择题: 图(a)中B

图(b)中C
A. P1、P2、P3都是非阻塞节点,该图可以化简,是非死锁的
B. P1、P2、P3都是阻塞节点,该图不可以化简,是死锁的
C. P2是阻塞节点,P1、P3是非阻塞节点,该图可以化简,是非死锁的
D. P1、P2是非阻塞节点,P3是阻塞节点,该图不可以化简,是死锁的

★ 如何看进程资源图呢?
P:进程    R:一类资源     R中的圆圈数:该类资源有几个
★ 字母箭头都是啥意思?
R→P(R指向P):分配一份R类资源給进程P
P→R(P指向R):进程P申请一份R类资源
★ 判断一个进程节点是否堵塞?
读图时,先看资源分配R→P,再看资源申请P→R
【注意】
读图时,不要将同时存在R→P、P→R双向箭头的情况理解成:
P先申请一个资源,R再分配一个资源给P!

可能存在的情况:
① R中所有资源分配出去了(R→P),而此时还有进程P向R申请资源(P→R)
此时申请资源R的进程P:成为阻塞节点
② R中所有资源没有全部都分配出去了(R→P),而此时还有进程P向R申请资源(P→R)
此时申请资源R的进程P:成为非阻塞节点

★ 判断一个进程资源图是否是死锁的?
如果所有节点都是阻塞的———此进程图不可以化简,是死锁的
如果有节点不是阻塞的————此进程图可以花间,是非死锁的
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
★[将非阻塞节点周围的箭头删除,只保留阻塞节点的箭头,此时观察在图中原来的阻塞节点是否阻塞]★
图a
R1一共有2个资源,给P1、P2各分配一个,已经无可分配资源
此时P2还向R1申请1个资源,因为没有资源可以申请了,所以P2会阻塞;

同理,R2一共有3个资源,给P1分配1个、P2分配2个,已经无可分配资源
此时P1还向R2申请1个资源,因为没有资源可以申请了,所以P1也会阻塞;

因为P1、P2节点都阻塞了,所以此图无法化简,是死锁的
图b
R1一共有2个资源,给P1、P3各分配一个,已经无可分配资源
此时P2还向R1申请1个资源,因为没有资源可以申请了,所以P2会阻塞;

R2一共有3个资源,给P2、P3各分配一个,还剩1个可分配资源
此时P1向R2申请1个资源,因为还有可分配资源,所以P1不会阻塞;
(此时P3也向R2申请1个资源,同理P3也不会阻塞)
【这里要特别注意:P1、P3虽然同时都在申请剩下的1个资源,但是并不意味着这个资源申请已经被响应了,从而造成死锁的现象】
--------------------------------------------------------------------------
R1出去两个资源,R1资源数为0,此时P2申请R1资源,P2阻塞。R2总资源数3,出去2个资源。P1,P3申请R2资源,P1,P3非阻塞。[R2资源有3个,已分配2个,P3申请1个R2资源可以得到满足,故进程P3可以进行完毕释放其占有的资源。这样可以使得P1、P3都变成非阻塞节点,得到所需资源进行完毕,因此,该进程资源图是可以化简的]

@@ 在支持多线程的操作系统中,假设进程P创建了若干个线程,那么该进程中某线程的栈指针是不能被这些线程共享的。在同一进程中的各个线程都可以共享进程所拥有的资源,如访问进程地址空间中的每一个虚地址;访问进程所拥有的已打开文件、定时器、信号量机构等,但是不能共享进程中某线程的栈指针

@@ 假设系统采用PV操作实现进程同步与互斥。若n个进程共享两台打印机,那么信号量S的取值范围为 -(n-2) ~ 2。信号量初值等于资源数量,即为2,由于同时最多有2个进程访问打印机,其余进程必须处理等待状态,故S的最小值为 -(n-2)

·每个进程开始的时候执行P操作(信号量减一操作),结束的时候执行V操作(信号量加一操作)。P作挂起阻塞,V为激活唤醒。信号量的原则是从左到右,从上到下。
·假设系统采用PV操作实现进程同步与互斥,若n个进程共享两台打印机,那么信号量S的取值范围为-(n-2)~2。两台打印机,所以最多等待状态的进程数就是(n-2),
★★如果没有进程申请打印机,打印机的信号量就是2★★
信号量的值小于0,表示没有可用的资源,其绝对值表示阻塞队列中等待该资源的进程数。

·同步信号量,值为可用资源的个数。信号量的值小于0,则线程进行等待;信号量的值大于0,表示值为可用资源的个数。初始值为0。
·互斥信号量,只有两个值:0和1。0表示资源被占用,线程等待;1表示资源没有被占用,线程可以进入。初始值为1。

@@ 假设系统中有n个进程共享3台扫描仪,并采用PV操作实现进程同步与互斥。若系统信号量S的当前值为-1,**进程P1、P2又分别执行了一次P(S)操作,那么信号量S的值应为___。
[当有进程运行时,其他进程访问信号量,信号量就会减1。S=-1-2]
[当进程执行P(S)操作时,它试图将信号量S的值减去1。因此,如果
进程P1和P2都执行了一次P(S)操作**,并且信号量S的初始值为-1,那么它们会尝试将S的值减去1两次,结果是S的值变为-3。这是因为当信号量S的值为-1时,两个进程都尝试将其减去1,每次减去1后,S的值分别变为-2和-3。所以最终S的值为-3]

@@ 假设系统采用PV操作实现进程同步与互斥,若有n个进程共享一台扫描仪,那么当信号量S的值为-3时,表示系统中有3个进程等待使用扫描仪
[信号量不用减1是因为本题并没有进程运行]

@@ 假设某分时系统采用简单时间片轮转法,当系统中的用户数为n、时间片为q时,系统对每个用户的相应时间T=(n*q) [在分时系统中是将把CPU的时间分成很短的时间片轮流地分配給各个终端用户]

@@ 如果系统采用信箱通信方式,当进程调用Send原语被设置为”等信箱”状态时,其原因是指定的信箱中存满了信件;因为Send原语是发送原语,如果系统采用信箱通信方式,那么当进程调用Send原语被设置成”等信箱“状态时,意味着指定的信箱存满了信件,无可用空间

@@ 绝对路径从根目录\开始,本题book2.doc的绝对路径为 \MyDrivers\user2\
相对路径从当前目录下一级开始,本题book2.doc的相对路径为 user2\

@@ 直接索引即索引直接指向物理块,可用表示逻辑块号范围:0-4号
一级索引即索引节点指向的物理块用来存放地址项,可用表示256个地址项,即256个物理块
二级索引即索引节点指向的物理块,存放的是一级索引的地址块地址。一共有256个地址块用来存放以及索引

@@ 某文件系统采用多级索引结构。若磁盘块的大小为1k字节,每个块号占3字节,那么采用二级索引时的文件最大长度为116281k字节 。
磁盘块的大小为1KB,每个块号需占3B,因此一个磁盘物理块可存放1024/3=341个块号(取整)
采用一级索引时的文件最大长度为:341×1024/1024=341KB
采用二级索引时的文件最大长度为:341×341×1024/1024=116281KB

@@ 若系统正在将系统目录文件修改的结果写回磁盘时系统发生崩溃,则对系统的影响相对较大。
影响文件系统可靠性因素之一是文件系统的一致性问题。很多文件系统是先读取磁盘块到主存,在主存进行修改,修改完毕再写回磁盘。 一致性检查包括 块的一致性检查文件的一致性检查

@@ 双击”xxx.jpg”文件名时,系统会自动通过建立的文件关联来决定使用什么程序打开该图像文件

文件管理

@@ 若系统正在将目录文件修改的结果写回磁盘时系统发生崩溃,则对系统的影响相对较大。很多文件系统是先读取磁盘块到主存,在主存进行修改,修改完毕再写回磁盘。

@@ 若某计算机系统的I/O接口与主存采用统一编址,则输入输出操作是通过访存指令来完成的。
常用的I/O接口编制方法有两种:
一是内存单元统一编址[将I/O接口中有关的寄存器或存储部件看作存储器单元,与主存中的存储单元统一编址。内存地址和接口地址统一在一个公共的地址空间里,对I/O接口的访问就如同对主存单元的访问一样,可以用访问内存单元的指令访问I/O接口]
二是单独编址[通过设置单独的I/O地址空间,为接口中的有关寄存器或存储部件分配地址码,需要设置专门的I/O指令进行访问。这种编址方式的优点是不占用主存的地址空间,访问主存的指令和访问接口的指令不同。]

@@ I/O设备管理软件一般分为4个层次。具体层次从上往下分别是**用户级I/O层、设备无关I/O层、设备驱动程序、中断处理程序、硬件** [用户五官射中硬件]

用户级I/O层:发出I/O调用
设备无关I/O层:设备名解析、阻塞进程、分配缓冲区
设备驱动程序:设置寄存器、检查设备状态
中断处理程序:I/O完成后唤醒设备驱动程序
硬件:完成具体的I/O操作

@@ DMA工作方式下,在主存与外设之间建立了直接的数据链路

@@ 计算机运行过程中,cpu需要与外设进行数据交换。采用**中断方式和DMA方式控制技术时,CPU与外设可并行工作。程序查询中断方式都需要CPU来执行程序指令进行数据的输入和输出,DMA方式则不同,这是一种不经过CPU而直接从内存存取数据**的数据交换模式。[DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传达,数据传达完毕再把信息反馈给CPU即可]

@@ 某磁盘有100个磁道,磁头从一个磁道移至另一个磁道需要6ms。文件在磁盘上非连续存放,逻辑上相邻数据块的平均距离为10个磁道,每块的旋转延迟时间及传输时间分别为100ms和20ms,则读取一个100块的文件需要18000ms。[访问一个数据块的时间应为寻道时间加旋转延迟时间及传输时间之和。根据题意,每块的旋转延迟时间及传输时间共需120ms,磁头从一个磁道移至另一个磁道需要6ms,但逻辑上相邻数据块的平均距离为10个磁道,即读完一个数据块到下一个数据块寻道时间需要60ms。通过上述分析,本题访问一个数据块的时间T=120ms+60ms=180ms,而读取一个100块的文件共需要18000ms。]

@@ 假设某磁盘的每个磁道划分成9个物理块,每块存放1个逻辑记录。逻辑记录R0,RI,…,R8存放在同一个磁道上,记录的安排顺序如下表所示:如果磁盘的旋转速度为27ms/周,磁头当前处在RO的开始处。若系统顺序处理这些记录,使用单缓冲区,每个记录处理时间为3ms,则处理这9个记录的最长时间为246ms:若对信息存储进行优化分布后,处理9个记录的最少时间为54ms

物理块 1 2 3 4 5 6 7 8 9
逻辑记录 R0 R1 R2 R3 R4 R5 R6 R7 R8

[因为系统使用的是单缓冲区,且顺序处理9个记录,每个记录处理时间为3ms,加上读写时间,总的时间就超过3ms了。而磁盘旋转一圈的时间为27ms,也就是说,当系统读取第0个记录后,正在处理的过程中,磁盘已经旋过了第1个记录。那么,要读取第1个记录,就需要磁盘再次旋转到第1个记录(即磁盘旋转1圈后,27+3=30ms)。同理,要读取第2个记录时,也需要等30ms。这样,要读取后面8个记录,需要8x30=240ms,同时加上处理第0个记录的时间(3ms)和处理第8个记录的时间(3ms),共需246ms。] [对于第二种情况,若对信息进行分布优化的结果如下所示:从上表可以看出,当读出记录RI并处理结束后,磁头刚好转至R2记录的开始处,立即就可以读出并处理,因此处理9个记录的总时间为:9X (3ms (读记录)+3ms (处理记录))=9X6ms=54ms]

@@ 在移臂调度算法中,先来先服务和最短寻找时间优先算法可能会随时改变移动臂的运动方向

@@ 概要设计文档的内容包括系统构架、模块划分、系统接口、数据设计四个方面

@@ 三层C/S体系结构由逻辑上互相分离的**表示层、业务层、数据层构成。其中表示层向客户提供数据,业务层实施业务相关数据规划,数据层**定义数据访问表中。

@@ 对软件过程的考虑:在整体上按照瀑布模型的流程实施项目开发,以方便对项目的管理;但在软件的实际创建中,则将软件系统按照功能分为许多增量构件,并以构件为单位逐个地创建与交付,直到全部增量构件创建完毕,并都被集成到系统之中交付用户使用。

模型 详情信息
增量模型 又称渐增模型,也称为有计划的产品改进模型,从一组给定的需求开始,通过构造一系列可执行中间版本来实施开发活动。第一个版本纳入一部分需求,下一个版本纳入更多的需求,直到系统完成。每个中间版本都需要执行必需的过程、活动和任务。增量模型是 [瀑布模型+原型] 进化模型的综合
[增量模型具有非常显著的优越性,但增量模型对软件设计有更高的技术要求,特别是对软件体系结构,要求它具有很好的开放性与稳定性,能够顺利地实现构件的集成;不必等到整个系统开发完成就可以使用可以使用较早的增量构件作为原型优先级最高的服务先交付, 这样最重要的服务接收最多的测试难点如何进行模块的划分。]
融合了瀑布模型的基本成分和原型实现迭代的特征,该模型随着日程时间的进展而交错的线性序列。
增量式开发的主要优点包括
1.由于能够在较短的时间内向用户提交一些有用的工作产品,因此能够解决用户的一些急用功能。
2.由于每次只提交用户部分功能,用户有较充分的时间学习和适应新的产品。
3.对系统的可维护性是一个极大的提高,因为整个系统是由一个个构件集成在一起的,当需求变更时只变更部分部件,而不必影响整个系统。
主要缺点包括
1.由于各个构件是逐渐并入已有的软件体系结构中的,所以加入构件必须不破坏已构造好的系统部分,这需要软件具备开放式的体系结构。
2.在开发过程中,需求的变化是不可避免的。增量模型的灵活性可以使其适应这种变化的能力大大优于瀑布模型和快速原型模型,但也很容易退化为边做边改模型,从而使软件过程的控制失去整体性。
3.增量包之间存在相交的情况且未很好处理
瀑布模型 将开发阶段描述为从一个阶段瀑布般地转换到另一个阶段的过程。将软件生存周期各个活动规定为线性顺序链接的若干阶段的模型[计划、分析、设计、编程、测试、维护]。规定了由前至后,相互衔接的固定次序,如同瀑布流水,逐级下落。是一种理想的开发模式,但缺乏灵活性,无法解决软件需求不明确或不准确的问题;不适用于开发初期对软件需求缺乏准确全面认识的情况优点:可强迫开发人员采用规范的方法;严格规定了各阶段必须提交的文档;要求每个阶段结束后,都要进行严格的审核。
原型模式 开发人员快速地构造整个系统或者系统的一部分以理解或澄清问题。增量模型是把软件产品作为一系列的增量构件来设计、编码、集成和测试,每个构件由多个相互作用的模块组成,并且能够完成特定的功能;并非所有的需求在系统开发之前都能准确地说明和定义。采用了动态定义需求的方法。适用于需求不明确的开发环境;可以有效地捕获系统需求
喷泉模型 喷泉模型是典型的面向对象生命周期模型,在开发过程中以用户需求为动力,以对象为驱动,适合于面向对象的开发方法;喷泉模型克服了瀑布模型不支持软件重用和多项开发活动集成的局限性;模型中的开发活动常常需要重复多次,在迭代过程中不断地完善软件系统;”喷泉”一词本身体现了迭代和无间隙特性。迭代意味着模型中的开发活动常常需要重复多次,在迭代过程中不断地完善软件系统;无间隙是指在开发活动之间不存在明显的边界。
螺旋模型 将开发活动和风险管理结合起来,以减小风险。将瀑布模型快速原型模型结合起来,还增加了风险分析;螺旋模型包含了四个方面的活动:制订计划、风险分析、实施工程、客户评估,螺旋模型适用于复杂的大型软件

@@ 若用户需求不清晰且经常发生变化,但系统规模不太大且不太复杂,则最适宜采用原型化开发方法
对于数据处理领域不太复杂的软件,若系统规模不太大且不太复杂,需求变化也不大,则最适宜采用结构化开发方法

@@ 软件工程的基本要素包括方法、工具、过程

@@ 在概要设计阶段选择适当的解决方案,将系统分解为若干个子系统,建立整个系统的体系结构。
软件设计的任务是基于需求分析的结果建立各种设计模型,给出问题的解决方案。
从工程管理的角度,可以将软件设计分为两个阶段:概要设计阶段详细设计阶段
概要设计阶段进行软件体系结构的设计、数据设计和接口设计;
详细设计阶段进行数据结构和算法的设计。面向对象设计方法中,概要设计阶段进行体系结构设计、初步的类设计/数据设计、结构设计:详细设计阶段进行构件设计。结构化设计和面向对象设计是两种不同的设计方法,结构化设计根据系统的数据流图进行设计,模块体现为函数、过程及子程序:面向对象设计基于面向对象的基本概念进行,模块体现为类、对象和构件等。

@@ “软件产品必须能够在3秒内对用户请求作出响应”属于软件需求中的非功能需求
软件需求是软件系统必须完成的事以及必须具备的品质。软件需求包括功能需求、非功能需求和设计约束三个方面的内容。功能需求是所开发的软件必须具备什么样的功能:非功能需求是指产品必须具备的属性或品质,如可靠性、性能、响应时间和扩展性等等;设计约束通常对解决方案的一些约束说明。”软件产品必须能够在3秒内对用户请求作出响应”主要表述软件的响应时间,属于非功能需求

@@ 软件复杂性度量是软件度量的一个重要分支。参数有很多主要包括:
规模。即指令数或者源程序行数;②难度。通常由程序中出现的操作数所决定的量来表示;
结构。通常用与程序结构有关的度量来表示;④智能度。即算法的难易程度

@@ 基于构件的软件开发,主要强调在构建软件系统时复用已有的软件”构件”,在检索到可以使用的构件后,需要针对新系统的需求对构件进行合格性检验、适应性修改,然后集成到新系统中

@@ 程序的三种基本控制结构式顺序、选择和重复

@@ 软件工程每一个阶段结束前,应该着重对可维护性进行复审。在系统设计阶段的复审期间,应该从容易修改、模块化和功能独立的目的出发,评价软件的结构和过程。可维护性式所有软件都具有的基本特点,必须在开发阶段保证软件具有可维护性的特点。

@@ I/O软件隐藏了I/O操作实现的细节。I/O软件向用户提供的式逻辑接口。I/O软件将硬件与较高层次的软件隔离开来,而最高层软件向硬件提供一个友好的、清晰的、统一的接口,方便用户使用。

@@ 在面向对象技术构建软件系统时,很多敏捷方法都建议的一种重要的设计活动是重构,它是一种重新组织的技术,可以简化构件的设计而无需改变其功能或行为

@@ 软件风险一般包含不确定性损失救火危机管理是对不适合但经常采用的软件风险管理策略,已知风险未知风险是对软件风险进行分类的一种方式。员工预算是在识别项目风险时需要识别的因素

@@ 在传值方式下是将实参的值传递给形参,因此实参可以是表达式、常量、变量、数组元素,这种信息传递是单方向的,形参不能再将值传回给实参。在传地址方式下,需要将实参的地址传递给形参,因此实参必须是变量、数组名或数组元素。不能是表达式或常量。在这种方式下,被调用函数中对形式参数的修改实际上就是对实际参数的修改,客观上可以实现数据的双向传递。

@@ 编译是将高级语言源程序翻译成机器语言程序,反编译是编译的逆过程。反编译通常不能把可执行文件还原成高级语言源代码,只能转换成功能上等价的汇编程序。

@@ 动态语言是指程序在运行时可以改变其结构,例如新的函数可以被引进、已有的函数可以被删除等在结构上的变化等。动态语言的类型检查是在运行时进行的,其有点是方便阅读,不需要非常多的与类型相关的代码;缺点是不方便调试。脚本语言代表一套与系统程序设计语言不同的协定。更适合在联系复杂的应用程序中进行胶着(粘合),一般都是面向字符的。脚本语言都是动态语言,而动态语言都是解释型语言

@@ WinXP支持FAT、FAT32、NTFS的文件系统格式化卷,而Win11默认仅提供NTFS格式 (ReFS主要面向企业级存储)

阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.