开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

JavaWeb——通俗易懂的servlet工作原理分析

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

前景知识

Servlet容器,web容器

  • Servlet容器也叫做Servlet引擎,是Web服务器或应用程序服务器的一部分,用于在发送的请求和响应之上提供网络服务,Tomcat是一个免费的开放源代码的Servlet容器。
  • web容器是一种服务程序,在web服务器一个端口就有一个提供相应服务的程序,而这个程序就是处理从客户端发出的请求,如JAVA中的Tomcat容器,ASP的IIS或PWS都是这样的容器。一个服务器可以有多个容器。

Servlet容器和Web容器的区别:

  • Sevrlet容器是用来管理servlet的生命周期,
  • web容器是用来管理和部署Web应用的。
  • ==Tomcat就是一个开源的Servlet容器,也是一个web容器==

工作原理

在这里插入图片描述

1.启动Tomcat,此时Tomcat作为Servlet容器,会==创建并初始化==Servlet,每个 Servlet 只被初始化一次

2.浏览器向Web容器(Tomcat)发送HTTP请求

3.如果Http请求是==首次==通过Web服务器(Tomcat)访问Servlet,将会把我们编写的实现了servlet接口的类由java文件编译为class文件。(所以如果你的web应用程序非常庞大,应用了非常多的Servlet,第一次运行会非常慢,后面就会越来越快)

4.当访问到达时,Servlet容器创建HttpServletRequest和HttpServletResponse对象,并且将请求数据放入HttpServletRequest中

  • 请求格式:请求行(request line)、请求头部(header)、空行和请求数据
  • 响应格式:状态行、消息报头、空行和响应正文。

5.Servlet容器(Tomcat)根据传递的url通过匹配web.xml中配置的Servlet路径,找到对应的Servlet实现类,传递请求和响应对象

6.请求和响应对象调用 service(ServletRequest req, ServletResponse res)方法

7.请求对象里的数据会进入我们重新实现的service()方法里,请求经过实现后变成响应交给响应对象,然后返回给Servlet容器(Tomcat)

  • service(ServletRequest req, ServletResponse res)里具体的方法是由我们的实现类来写的
  • 我们编写的实现类重写这些方法是为了:
    • 接收并处理请求
    • 给出响应的信息

8.Servlet容器(Tomcat)解析响应数据,根据HTTP通讯协议,返回给浏览器,浏览器解析并展示数据。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaWeb——IDEA中使用Maven的教程

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

前言(必看)

==Maven3.6.2及其以上不适配 IDEA 2019.2 需要跟换为低一点版本,这个坑踩的太狠了==。

下载Maven旧版本的方法(以3.6.1为例):

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

点我查看超详细的安装和环境配置教程

正文

创建一个MavenWeb项目

1.打开IDEA,点击创建一个新项目

在这里插入图片描述

2.创建一个MavenWeb项目

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.等待项目初始化完毕
在这里插入图片描述
在这里插入图片描述

4.成功导入到maven仓库

在这里插入图片描述

5.IDEA中Maven的设置

在这里插入图片描述
在这里插入图片描述

下面情况只有web应用下独有

在这里插入图片描述

替换webapp版本

修改Mavenweb项目的配置文件,以保持和Tomcat的版本一致

1
2
3
4
5
6
7
8
9
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"
metadata-complete="true">
</web-app>

在这里插入图片描述

创建一个普通的Maven项目

在这里插入图片描述
在这里插入图片描述

然后一个干净的Maven项目就生成了

在这里插入图片描述

main\java:放置java源代码

main\resources:放置一些配置文件

test\java:测试使用

标记文件夹的功能

方式一

在这里插入图片描述

方式二

Project Structure–>Modules–>Sources里设置

标记完Maven就可以开始用了

在这里插入图片描述

pom文件

pom.xml是Maven的核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<!--Maven版本和头文件-->
<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>

<!--Maven的GAV(坐标)-->
<groupId>com.cheng</groupId><!-- groupId:定义当前Maven组织名称 -->
<artifactId>javaweb-02-maven</artifactId><!-- artifactId:定义实际项目名称 -->
<version>1.0-SNAPSHOT</version><!-- version:定义当前项目的当前版本 -->

<!--package:项目的打包方式
jar:java应用
war:JavaWeb应用
-->
<packaging>war</packaging>

<name>javaweb-02-maven Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<!--配置-->
<properties>
<!--编码-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--编译版本,可以改成1.8-->
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<!--项目依赖-->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

<!--项目构建用的东西-->
<build>
<finalName>javaweb-02-maven</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

添加新的配置(导入jar包)

在这里插入图片描述

资源导出失败的问题

Maven由于约定大于配置,所以可能会遇到写的配置文件无法导出或者生效的问题,解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<!--在build中配置resources,来防止我们资源导出失败的问题-->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.properties</exclude>
<exclude>**/*.xml</exclude>
</excludes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

maven目录树

在这里插入图片描述

解决IDEA每次都要重复配置Maven的问题

在全局默认配置中配置

在这里插入图片描述

IDEA中配置Tomcat

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

解决下角的警告问题

artifacts的定义:==即编译后的Java类,Web资源等的整合,用以测试、部署等工作==。再白话一点,就是说某个module要如何打包,例如war exploded、war、jar、ear等等这种打包形式。某个module有了 Artifacts 就可以部署到应用服务器中了

为什么会有这个警告:我们如果要访问一个网站,必须要配置artifacts

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

启动Tomcat服务器

在这里插入图片描述

启动成功:

在这里插入图片描述

注意看网页路径后面的cheng,说明访问路径写的是cheng

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

把Java多学一点 ------ 线程中断

发表于 2021-11-29

对于比较耗时的任务,一般会采用专门的线程来执行,如果在执行过程中想要取消这类的任务的执行,那就需要借助Java线程中断机制。

​​为什么是线程中断而不是线程停止?

线程停止是使得线程终止,线程状态变为TERMINATED,但是对于目标工作者线程来说,可能除了用户想要取消的任务,还有其他任务要执行,或者说在终止线程之前还有工作需要处理,而线程中断则是将对中断的处理交给目标线程,由目标线程对发送给自己的中断请求进行响应,将线程的中断处理转为目标线程主动处理。

线程停止是目标简单但是实现并不简单的一件事情,Java标准库并没有可以直接停止线程的API(stop方法早已被废弃),停止线程的时候还有一些额外的细节需要考虑。

Java线程中断机制其实是线程间的一种协作方式:发起线程请求目标线程停止其正在执行的操作,而目标线程在收到发起线程的中断请求后进行相应的处理,处理的方式我们下文会提到。

中断实现:中断标记

那么发起线程发送中断请求给目标线程是如何实现的呢,这里涉及到一个重要的状态变量:中断标记。

Java平台会为每个线程对象(Thread实例)维护一个布尔类型的状态变量,被称作中断标记,该变量用于表示相应线程是否收到了中断,中断标记为true,则表示收到了中断请求。

关于线程中断操作,Thread类提供了三个方法:

1
csharp复制代码public void interrupt();

该方法为线程中断方法,发起线程调用目标线程的interrupt方法可以将目标线程的中断标记置为true。

1
java复制代码public static boolean interrupted()

通过调用interrupted方法检测线程的中断状态并清除中断状态。

1
arduino复制代码public boolean isInterrupted();

isInterrupted方法来判断线程是否收到中断,线程的中断状态不受该方法影响。

中断响应

目标线程在收到中断请求后所执行的操作称作中断响应。目标线程对中断的响应一般包括:

  • 无响应。发起线程通过调用目标线程interrupt()方法请求中断,设置目标线程的中断标记为true,但是目标线程无法对中断请求进行响应。IntputStream.read()、ReentrantLock.lock()和以及申请内部锁等方法就属于这种类型。
  • 取消任务的执行。发起线程通过调用目标线程的interrupt()方法来中断目标线程,目标线程在检测到中断那一刻所执行的任务被取消,但这并不会影响目标线程继续处理其他任务。
  • 工作者线程停止。发起线程通过调用目标线程的interrupt()方法会使得目标线程终止,线程状态变为TERMINATED,相当于线程停止。

InterruptedException

Java标准库中的许多阻塞方法对中断的响应方式都是抛出InterruptedException等异常,所以在应用层代码中,通常可以通过对InterruptedException等异常进行处理的方式来实现中断响应。

Java标准库中对中断的处理

1
2
3
4
5
6
7
8
9
scss复制代码public void lock() {
   sync.acquire(1);
}
​
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

该方法为ReentrantLock类的lock方法,该方法中没有发现进行判断线程中断标记的代码(没有调用isInterrupted()或interrupted()方法),即该方法不会对线程中断请求作任何响应,也就是说如果目标线程调用了该阻塞方法,如果有线程对该线程发送中断请求,相应线程不能对其作出响应。

ReentrantLock类还提供了一个同类方法:lockInterruptibly()

1
2
3
4
5
6
7
8
9
10
java复制代码public void lockInterruptibly() throws InterruptedException {
   sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

可以看到该方法在进行阻塞操作之前,会判断是否收到了中断请求,如果收到了中断请求便抛出InterruptedException异常,并且在抛出InterruptedException异常之前,会将中断标记置为false。

wait方法、sleep方法、join方法等对中断的响应也都是抛出InterruptedException异常,并且在抛出异常之前,会将中断标记置为false。

java-多线程-wait方法对中断响应方式

打断暂停

如果当发起线程向目标线程发送中断请求的那一刻目标线程由于调用阻塞方法而被暂停,生命周期状态为WAITING或BLOCKED,那么此时Java虚拟机可能会设置线程中断标记并将该线程唤醒,从而使得该目标线程被唤醒后继续执行的代码再次得到响应中断的机会,所以这种情况下响应中断的阻塞方法依然可以抛出InterruptedException异常,并在此之前将线程中断标记为空。

响应中断

前面说到,Java标准库中大部分阻塞方法/操作都是通过抛出InterruptedException等异常,那么在应用层代码通常可以通过对这些异常来进行响应中断请求。

对InterruptedException异常的处理方式一般包括以下几种:

  • 不捕获异常,将异常继续抛给上层代码。
  • 捕获InterruptedException并做一些中间处理,然后再将异常抛给上层代码。
  • 捕获InterruptedException并在捕获异常后中断当前线程。

总结:对InterruptedException的处理,如果当前代码不知道如何处理终端,要么将异常继续抛给上层代码由上层代码处理,要么继续保留中断标志为true,由其他代码来处理。

​​当线程在捕获到InterruptedException后就可以终止的情况下才可以吞没异常,其他情况切记不能吞没异常,即在捕获到interruptedException后既不重新抛出也不保留中断标志。则可能导致目标线程无法被终止。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习剑指offer:第17天

发表于 2021-11-29

最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

1
2
ini复制代码输入: arr = [3,2,1], k = 2
输出: [1,2] 或者 [2,1]
1
2
ini复制代码输入: arr = [0,1,2,1], k = 1
输出: [0]

限制:

  • 0 <= k <= arr.length <= 10000
  • 0 <= arr[i] <= 10000

leetcode-cn.com/problems/zu…

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
for(int i=0; i<arr.length; i++){
queue.offer(arr[i]);
}
int[] result = new int[k];
for(int i=0; i<k; i++){
result[i] = queue.poll();
}
return result;
}
}

数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。
1
2
3
4
css复制代码输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
1
2
3
4
css复制代码输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制:

  • 最多会对 addNum、findMedian 进行 50000 次调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码class MedianFinder {

PriorityQueue<Integer> left;
PriorityQueue<Integer> right;

/** initialize your data structure here. */
public MedianFinder() {
left = new PriorityQueue<>(Comparator.reverseOrder());
right = new PriorityQueue<>();
}

public void addNum(int num) {
left.add(num);
right.add(left.poll());
if(left.size() +1 < right.size()){
left.add(right.poll());
}
}

public double findMedian() {
if(right.size() > left.size()){
return right.peek();
}
return (double)(left.peek()+ right.peek()) /2;
}
}

/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

运维思索:自动化运维体系如何入手? 需求 运维框架 运维依据

发表于 2021-11-29

需求

运维是事件驱动,还是自驱动可能是我们在运维工作中不太关注的问题。事件驱动让运维止步于故障,而自驱动让运维不止于建设。持续性的运维建设就需要一套自动化的运维体系,那么我们应该从何入手?

其实前期《运维思考》一系列文章已经给我们答案了,就是从运维框架入手分层建设、打好基础,记住“万丈高楼平地起,勿在浮沙筑高台”。

运维框架

运维体系架构.png

通常讲到运维建设,我们脑海中首先浮现的是“一团麻”,因为这不是一个人、一个岗位的工作,而是一整个团队的工作;所以我们将“这团麻”进行由底层向上可划分为:

  • IT基础设施层

IT基础设施层,主要由基础运维团队负责,主要包括存储、网络、服务器、安全设备等硬件设施;

  • 数据层

数据层,主要由DBA团队、大数据团队负责,主要包括数据库、缓存、数仓等;

  • 应用层

应用层,主要由应用运维团队负责,主要包括基础服务、业务应用、中间件等;

  • 管理层

管理层,主要由配置管理团队、安全团队、应用运维团队负责,主要包括各种自动化操作、安全管理、监控管理等;

  • 展示层

展示层,主要由各团队综合管理,主要包括各种管理工具、监控工具等;

通过对运维框架的分解,对各种资源的逻辑隔离,让各个团队明确当前运维建设中的现状与不足。 如果我们能做到对运维框架的持续性关注,通过图片就可以明晰的知道哪个团队的不足,以及日后各团队的重点发力方向。

运维依据

如果你觉得运维框架还不够细致,那么针对框架中各个层次的工作拆解就来了,我们在此将其称之为运维依据。

针对这些个运维依据,我们可以展开一些列的针对性措施,如制定规范、自动化流程,如此就能够不断丰富各个团队的制度、规范、流程,何乐而不为?

自动化运维体系.png

1.基础设施层

在基础的硬件设施管理之上,比较重点的工作是

  • 网络分区与隔离

网络分区应考虑互联网接入区、普通生产区、数据区、外联区等各个区域,保证各区域的合理接入。

网络隔离对测试、准生产、生产环境各环境进行隔离,避免访问权限混乱。

  • CMDB资产纳管

CMDB用于管理基础设施层的各项资产,为上层应用提供数据支撑。使用CMDB一定要和业务应用紧密结合,一旦脱离于业务使用,那么CMDB将成为花瓶。

相关场景可参考《运维思索:接地气的运维自动化建设》。

  • 内部dns

通过内部dns可以将应用与IP解耦,一旦ip变更则不需要变更代码,生产环境应该尽量少做此种类型变更操作。

  • 服务器快速上架

为满足业务日益增长的需求,应该具备服务器快速上架、资产实时记录至CMDB等一系列自动化流程。

  • 网络权限变更

根据应用需求,快速登记并开通网络权限。

等等。

2.数据库

数据库除了特有的集群外,可以考虑数据库工单、sql审核优化等流程。

3.系统应用

  • 容量规划

容量规划是指根据业务用户流量增长、现有容量等一定的基础数据之上进行周期性的评估,如果有条件的话可结合压测实际情况,这样数据会更准确。通过容量规划可有效控制服务器规范,避免资源溢出。

  • 环境维护与部署

为避免因环境差异导致的问题,各环境应用部署需要遵循统一的目录规范,统一的自动化部署方式,分离的应用配置文件。

等等

4.配置管理

  • 统一账号管理

所有和用户登录相关的平台、管理工具,尽量接入ldap统一账号管理,这样一个账号可以实现所有系统的统一登录。

  • 自动化配置中心

在此秉承基础设施即代码的思想,通过ansible作为配置中心,在操作系统层面实现系统初始化、环境初始化、组件初始化、自动化备份等中心化管理,各环境交付统一规格的服务器。

  • 流程管理

结合jira等工作流工具实现操作的流程化管理。

等等

5.CI/CD

基于统一的运维规范前提下,CI/CD可以真正的做到将以上各个层面的想法、解决方案进行落地。因此CI/CD能力很大程度上决定了我们自动化运维的高度。

  • 持续集成

代码质量测试、单元测试、打包测试、自动化测试等。

  • 操作系统交付

遵循统一的运维规范,交付统一规格的操作系统,完成对运维平台各个管理节点的资源注册。

  • 版本发布

支持版本平滑发布、回滚、重启等。

  • 自动打包

Android/IOS 自动打包并上传至应用商店。

6.监控系统

  • 系统建设

多维度收集、分析监控数据,实现不同层面的告警;

对于多维度的数据能够进行分析,实现故障自愈;

  • 监控管理

监控并不是只要做到告警进行了,而是要做到告警的准确性,因此对告警级别、告警收敛、故障自愈策略等的管理需要我们进行重点关注。

7.安全防护

通过必要的WAF、IDS、防火墙等安全设备进行安全防护、流量分析外,还要结合安全渗透去主动发现问题。

8.数据分析

通过对应用数据、业务数据、运营数据进行集中分析、展示,帮助我们更好的了解系统运行状况。

总结

通过以上各个层面的运维框架和运维依据,希望大家能够结合实际情况进行头脑风暴,做到不止于此。

当然自动化运维建设不是一蹴而就的,需要结合规范、制度、流程去逐步实现。

记住运维建设是过程,不仅仅是目标,我们需要跟随技术潮流趋势,持续的优化与丰富这个过程。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

access_token VS refresh_token

发表于 2021-11-29

众所周知, 在 OAuth 2.0 授权协议中, 也有两个令牌 token , 分别是 access_token 和 refresh_token, 为什么已经有了 access_token, 还需要 refresh_token 呢?

我们先看下面两者的介绍

  • access_token

访问令牌, 它是一个用来访问受保护资源的凭证

  • refresh_token

刷新令牌, 它是一个用来获取access token的凭证

下面是 OAuth 2.0 中的 token 工作流程图

回归主题,

这两个令牌的主要区别如下:

  • access_token 时效短, refresh_token 时效长, 比如 access_token 有效期1个小时, refresh_token 有效期1天
  • access_token 是授权服务器一定颁发的, 而 refresh_token 却是可选的
  • access_token 过期后, 可以使用 refresh_token 重新获取, 而 refresh_token 过期后就只能重新授权了, 也没有 refresh_refresh_token
  • access_token 和 资源服务器和授权服务器交互, 而 refresh_token 只和 授权服务器交互
  • access_token 颁发后可以直接使用, 而使用 refresh_token 需要客户端秘钥 client_secret

接下来, 我们继续看两个令牌在下面场景的应用, 假设有一个用户需要在后台管理界面上操作6个小时。

1 颁发一个有效性很长的 access_token, 比如 6 个小时, 或者可以更长, 这样用户只需要刚开始登录一次, access_token 可以一直使用, 直到 access_token 过期, 然后重复, 这种是不安全的, access_token 的时效太长, 也就失去了本身的意义。

2 颁发一个1小时有效期的 access_token, 过期后重新登录授权, 这样用户需要登录 6 次, 安全倒是有了, 但是用户体验极差

3 颁发1小时有效期的 access_token 和6小时有效期的 refresh_token, 当 access_token 过期后(或者快要过期的时候), 使用 refresh_token 获取一个新的 access_token, 直到 refresh_token 过期, 用户重新登录, 这样整个过程中,用户只需要登录一次, 用户体验好。

access_token 泄露了怎么办? 没关系, 它很快就会过期。

refresh_token 泄露了怎么办? 没关系, 使用 refresh_token 是需要客户端秘钥 client_secret 的。

4 用户登录后, 在后台管理页面上操作1个小时后, 离开了一段时间, 然后 5个小时后, 回到管理页面继续操作, 此时 refresh_token 有效期6个小时, 一直没有过期, 也就可以换取新的 access_token, 用户在这个过程中, 可以不用重复登录。但是在一些安全要求较高的系统中, 第二次操作是需要重新登录的, 即使 refresh_token 没有过期, 因为中间有几个小时, 用户是没有操作的, 系统猜测用户已离开, 并关闭会话。

所以, 得出的结论是, refresh_token 是一个很巧妙地设计, 提升了用户体验的同时, 又保证了安全性。

另外, 在 OAuth 2.0 安全最佳实践中, 推荐 refresh_token 是一次性的, 什么意思呢? 使用 refresh_token 获取 access_token 时, 同时会返回一个 新的 refresh_token, 之前的 refresh_token 就会失效, 但是两个 refresh_token 的绝对过期时间是一样的, 所以不会存在 refresh_token 快过期就获取一个新的, 然后重复,永不过期的情况。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Springboot系列(十) mybatis之xml映射文

发表于 2021-11-29

👨‍🎓作者:bug菌

✏️博客:CSDN、掘金、infoQ、51CTO等

🎉简介:CSDN博客专家,C站历届博客之星Top50,掘金/InfoQ/51CTO等社区优质创作者,全网合计8w粉+,对一切技术感兴趣,重心偏Java方向;硬核公众号「 猿圈奇妙屋」,欢迎小伙伴们的加入,一起秃头,一起变强。

..

✍️温馨提醒:本文字数:1999字, 阅读完需:约 5 分钟

嗨,家人们,我是bug菌呀,我又来啦。今天我们来聊点什么咧,OK,接着为大家更《springboot零基础入门教学》系列文章吧。希望能帮助更多的初学者们快速入门!

小伙伴们在批阅文章的过程中如果觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐+关注👨‍🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️

一、前言🔥

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

在利用mybatis进行开发的时候,编写sql时可能少不了>=、<等比较符号,但是在mapper映射文件中直接使用是不行的,会报错,这时就需要将这些特殊符号进行一个转换,有两种转换方式,大家请看:

第一种:直接转义

1
2
3
4
5
perl复制代码大于  => &gt;
大于等于 => &gt;=
小于 => &lt;
小于等于 => &lt;=
不等于 => &lt;&gt;

具体咱们一个一个测试,好吧

原义sql:

1
sql复制代码select * from user where id>2;

则在UserMapper.xml 中,得这么写

1
sql复制代码select * from user where id &gt;2

测试一下 大于符号:

结果如下,控制台明显是进行了符号转义。

再测试一个 小于等于 符号:

原义sql:

1
sql复制代码select * from user where id<=2;

则在UserMapper.xml 中,得这么写

1
sql复制代码select * from user where id &lt;=2

结果如下,控制台明显也是进行了符号转义。

好啦,剩下的就你们自己写着试试啦,我这就不一一赘述了啊,好伐?

第二种:

1、先大致给大家介绍一下,如下判断符号对于的转义符.也很好记,大于小于跟数学符号一致,更换[ ]中的符号即可。

大于 =>

大于等于 =>

小于 =>

小于等于 =>

不等于 =>

2、给大家举例演示一下吧!

原义sql:

1
sql复制代码select * from user where id<=2;

.xml中使用:

1
xml复制代码select * from user where id  <![CDATA[ <= ]]> 2

执行结果如下:

如上执行sql打印结果与要执行的sql执行逻辑一致,说明转义成功了。再举例一个不等于 的sql吧,大家请看。

原义sql:

1
sql复制代码select * from user where id !=2;

.xml中使用:

1
xml复制代码select * from user where id  <![CDATA[ <> ]]> 2

执行结果如下:

综上,使用如上两种,都很简单,大家根据自己的代码习惯选择而定,两种方式都没啥太大的区别,唯一区别就是写法略微不同而已啦。

… …

OK,以上就是这期所有的内容啦,如果有任何问题欢迎评论区批评指正,咱们下期见。

五、往期推荐

  • springboot系列(一):如何创建springboot项目及启动
  • springboot系列(二):yaml、properties两配置文件介绍及使用
  • springboot系列(三):多环境切换,实例演示
  • springboot系列(四):stater入门
  • springboot系列(五):史上最最最全springboot常用注解
  • springboot系列(六):mysql配置及数据库查询
  • springboot系列(七):如何通过mybatis-plus实现接口增删改查
  • springboot系列(八):mybatis-plus之条件构造器使用手册
  • springboot系列(九):mybatis-plus之如何自定义sql
  • springboot系列(十):mybatis之xml映射文件>、<=等特殊符号写法
  • springboot系列(十一):实现多数据源配置,开箱即用
  • springboot系列(十二):如何实现邮件发送提醒,你一定得会(准备篇)
  • springboot系列(十三):如何实现发送普通邮件?你一定得会
  • springboot系列(十四):如何实现发送图片、doc文档等附件邮件?你一定得会
  • springboot系列(十五):如何实现静态邮件模板发送?你一定得会
  • springboot系列(十六):如何实现发送邮件提醒,附完整源码
  • springboot系列(十七):集成在线接口文档Swagger2
  • springboot系列(十八):如何Windows安装redis?你玩过么
  • springboot系列(十九):如何集成redis?不会我教你
  • springboot系列(二十):如何通过redis实现手机号验证码功能
  • … …

文末🔥

如果还想要学习更多,小伙伴们可关注bug菌专门为大家创建的专栏《springboot零基础入门教学》,从无到有,从零到一!希望能帮助到更多小伙伴们。

我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

感谢认真读完我博客的铁子萌,在这里呢送给大家一句话,不管你是在职还是在读,绝对终身受用。

时刻警醒自己:

抱怨没有用,一切靠自己;

想要过更好的生活,那就要逼着自己变的更强,生活加油!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go(八)还不知道函数式选项模式?

发表于 2021-11-29

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

作者:lomtom

个人网站:lomtom.cn

个人公众号:博思奥园

你的支持就是我最大的动力。

Go系列:

  1. Go(一)基础入门
  2. Go(二)结构体
  3. Go(三)Go配置文件
  4. Go(四)Redis操作
  5. Go(五)Go不知道怎么用Gorm?
  6. Go(六)来来来,教你怎么远程调用
  7. Go(七)你说你不会并发?
  8. Go(八)还不知道函数式选项模式?

引入

为option 结构体进行初始化,因为其是私有的,即只能包内访问,所以需要编写一个构造函数。

1
2
3
4
5
go复制代码type option struct {
A string
B string
C int
}

构造函数

1
2
3
4
5
6
7
Go复制代码func newOption(a, b string, c int) *option {
return &option{
A: a,
B: b,
C: c,
}
}

使用的时候,直接调用该方法即可。

1
go复制代码option := newOption("a","b",10)

这样的代码看起来是没有问题的,并且使用起来也是非常方便的。

但是,如果此时需求发生变化,我不再需要三个参数的option,我只需要两个或者一个参数的option,怎么办呢?

而恰好Go语言不支持方法的重载,如果需要为一个结构体写多个初始化的构造函数,相同的方法名,不同的参数,这样的情况是不允许的,但往往这样情况的使用场景还是挺多的。

那么怎么办呢,只能在编写一个构造方法,并且命名为newOption1。

1
2
3
4
5
6
go复制代码func newOption1(a, b string) *option {
return &option{
A: a,
B: b,
}
}

这样一看,确实没问题,也满足了我们的需求,但是谁也不敢保证日后需求不再发生变动。

那么选项模式的优势就来了。

选项模式进行初始化

选项模式利用闭包以及不定参数来达到参数的可选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码type OptionFunc func(*option)

func WithA(a string) OptionFunc {
return func(o *option) {
o.A = a
}
}

func WithB(b string) OptionFunc {
return func(o *option) {
o.B = b
}
}

func WithC(c int) OptionFunc {
return func(o *option) {
o.C = c
}
}

首先自定义一个函数类型,函数的参数是*option,并且为三个参数编写该选项方法。

1
2
3
4
5
6
7
8
9
10
11
Go复制代码func newOption(opts... OptionFunc)(opt  *option) {
opt = &option{
A: "A",
B: "B",
C: 100,
}
for _, optFun := range opts {
optFun(opt)
}
return
}

修改其构造方法,这样将每一个参数都作为一个选项进行初始化,如果没有传入该参数的选项,即保留默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Go复制代码func TestGo7(t *testing.T) {
o := newOption()
fmt.Println(o)

o1 := newOption(
WithA("1"),
)
fmt.Println(o1)

o2 := newOption(
WithA("a"),
WithB("b"),
WithC(10),
)
fmt.Println(o2)
}

使用上,对结构体进行初始化时,可以选择初始化其一部分参数。

选项模式在Gorm中的应用

选项模式除了在结构体的初始化,还可以应用到哪方面呢?

恰好,对于数据库的查询操作往往也适合,在查询时,我们可能需要多个条件进行筛选,如果每种情况都写一个查询方法,不仅会使代码看起来很凌乱,并且写得多了,自己逻辑叶会变得很凌乱。

例如,这有一个字典表,拥有很多属性,如果对每个属性都需要进行筛选,那么情况就会有很多种,而利用选项模式只需要对每个字段编写一个选项即可完成多条件筛选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// DataDictionary [...]
type DataDictionary struct {
ID string `gorm:"primaryKey;column:id;type:varchar(255);not null" json:"-"`
DataType string `gorm:"column:data_type;type:varchar(255);not null" json:"dataType"` // 数据类型
DataKey string `gorm:"column:data_key;type:varchar(255);not null" json:"dataKey"` // 数据key
DataValue string `gorm:"column:data_value;type:varchar(255);not null" json:"dataValue"` // 数据value
SortNumber int `gorm:"column:sort_number;type:int;not null" json:"sortNumber"` // 排序编号
Level string `gorm:"column:level;type:varchar(255);not null" json:"level"` // 级别
Deleted bool `gorm:"column:deleted;type:tinyint(1);not null;default:0" json:"deleted"` // 是否删除
UpdateTime time.Time `gorm:"column:update_time;type:timestamp;not null" json:"updateTime"` // 更新时间
CreateTime time.Time `gorm:"column:create_time;type:timestamp;not null" json:"createTime"` // 创建时间
DomainID string `gorm:"column:domain_id;type:varchar(255)" json:"domainId"` // 主账号
}

// TableName get sql table name.获取数据库表名
func (m *DataDictionary) TableName() string {
return "data_dictionary"
}

编写自定义方法,编写options结构体用于存储选项关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type options struct {
query map[string]interface{}
}

type Option interface {
apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
f(o)
}

对DataType字段和DataKey字段编写查询选项方法。

1
2
3
4
5
6
7
8
9
10
11
go复制代码/****************************************  选项模式  ***********************************************************/

// WithDataType dataType获取 字典类别
func (obj *DataDictionaryMapper) WithDataType(dataType string) Option {
return optionFunc(func(o *options) { o.query["data_type"] = dataType })
}

// WithDataKey dataType获取 字典类别
func (obj *DataDictionaryMapper) WithDataKey(dataKey string) Option {
return optionFunc(func(o *options) { o.query["data_key"] = dataKey })
}

编写查询方法,并且将选项遍历放入最开始定义的结构体options中。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// GetByOption 功能选项模式获取
func (obj *DataDictionaryMapper) GetByOption(opts ...Option) (result []po.DataDictionary, err error) {
options := options{
query: make(map[string]interface{}, len(opts)),
}
for _, o := range opts {
o.apply(&options)
}
err = obj.DB.Where(options.query).Find(&result).Error
return
}

使用上直接将选项作为参数传入即可

1
go复制代码res, _:= c.GetByOption(c.WithDataType("position"))

这里你可能会纳闷,直接将options.query传入where为什么可以?

上面我们定义了options.query为map[string]interface{},我们点进Where方法源码中的tx.Statement.BuildCondition(query, args...)就可以看到,gorm会对参数进行类型判断,并且转换为sql。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
go复制代码switch v := arg.(type) {
case clause.Expression:
conds = append(conds, v)
case *DB:
if cs, ok := v.Statement.Clauses["WHERE"]; ok {
if where, ok := cs.Expression.(clause.Where); ok {
if len(where.Exprs) == 1 {
if orConds, ok := where.Exprs[0].(clause.OrConditions); ok {
where.Exprs[0] = clause.AndConditions(orConds)
}
}
conds = append(conds, clause.And(where.Exprs...))
} else if cs.Expression != nil {
conds = append(conds, cs.Expression)
}
}
case map[interface{}]interface{}:
for i, j := range v {
conds = append(conds, clause.Eq{Column: i, Value: j})
}
case map[string]string:
var keys = make([]string, 0, len(v))
for i := range v {
keys = append(keys, i)
}
sort.Strings(keys)

for _, key := range keys {
conds = append(conds, clause.Eq{Column: key, Value: v[key]})
}
case map[string]interface{}:
var keys = make([]string, 0, len(v))
for i := range v {
keys = append(keys, i)
}
sort.Strings(keys)

for _, key := range keys {
reflectValue := reflect.Indirect(reflect.ValueOf(v[key]))
switch reflectValue.Kind() {
case reflect.Slice, reflect.Array:
if _, ok := v[key].(driver.Valuer); ok {
conds = append(conds, clause.Eq{Column: key, Value: v[key]})
} else if _, ok := v[key].(Valuer); ok {
conds = append(conds, clause.Eq{Column: key, Value: v[key]})
} else {
// optimize reflect value length
valueLen := reflectValue.Len()
values := make([]interface{}, valueLen)
for i := 0; i < valueLen; i++ {
values[i] = reflectValue.Index(i).Interface()
}

conds = append(conds, clause.IN{Column: key, Values: values})
}
default:
conds = append(conds, clause.Eq{Column: key, Value: v[key]})
}
}

参考:
golang中函数类型
Go语言设计模式之函数式选项模式
Functional Options Pattern in Go
选项模式(option)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大话数据结构--图的遍历 74图的遍历

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

7.4图的遍历

图的遍历是和树的遍历类似,我们希望从图中某一顶点出 发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍(Traversing Graph)。

7.4.1深度优先遍历

深度优先遍历(Deep_First_Search),称为简称DFS。

主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。

实例如下,序号代表的是遍历的顺序

从根节点 1 开始遍历,它相邻的节点有 2,3,4,先遍历节点 2,再遍历 2 的子节点 5,然后再遍历 5 的子节点 9

此时就从 9 回退到上一个节点 5,看下节点 5 是否还有除 9 以外的节点,没有继续回退到 2,2 也没有除 5 以外的节点,回退到 1,1 有除 2 以外的节点 3,所以从节点 3 开始进行深度优先遍历

同理从 10 开始往上回溯到 6, 6 没有除 10 以外的子节点,再往上回溯,发现 3 有除 6 以外的子点 7,所以此时会遍历 7

从 7 往上回溯到 3, 1,发现 1 还有节点 4 未遍历,所以此时沿着 4, 8 进行遍历,这样就遍历完成了

img

这好像就是树的前序前序遍历啊实际上不管是前序遍历,还是中序遍历,亦或是后序遍历,都属于深度优先遍历。

7.4.2广度优先遍历

广度优先遍历(Breadth_ First Search), 又称为广度优先搜索,简称BFS

指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点

广度优先便利

它这个遍历类似图中的层序遍历

DFS 一般是解决连通性问题,而 BFS 一般是解决最短路径问题

#include<stdio.h>
#include<stdlib.h>
#define max 20
//边表节点
typedef struct node{
int adjvex;
struct node *next;
}eNode;
//头节点
typedef struct headnode{
char vertex;
eNode *firstedge;
}hNode;
//邻接表
typedef struct{
hNode adjlist[max];
int n,e; //顶点数,边数
}linkG;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
js复制代码#include<stdio.h>
#include<stdlib.h>
#define max 20
//边表节点
typedef struct node{
int adjvex;
struct node *next;
}eNode;
//头节点
typedef struct headnode{
char vertex;
eNode *firstedge;
}hNode;
//邻接表
typedef struct{
hNode adjlist[max];
int n,e; //顶点数,边数
}linkG;

//创建(邻接表)
linkG *creat(linkG *g,int c) //c为0表示无向图
{
int i,j,k;
eNode *s;
int n1,e1;
char ch;
g=(linkG *)malloc(sizeof(linkG));
printf("请输入顶点数及边数: ");
scanf("%d%d",&n1,&e1);
g->n=n1;g->e=e1;
printf("请输入顶点信息:");
getchar();
for(i=0;i<n1;i++)
{
scanf("%c",&ch);
g->adjlist[i].vertex=ch;
g->adjlist[i].firstedge=NULL;
}
getchar();
int i1,j1;
for(k=0;k<e1;k++)
{
printf("请输入对(i,j): ");
scanf("%d%d",&i1,&j1);
s=(eNode *)malloc(sizeof(eNode));
s->adjvex=j1;
s->next=g->adjlist[i1].firstedge;
g->adjlist[i1].firstedge=s;
if(c==0)
{
s=(eNode *)malloc(sizeof(eNode));
s->adjvex=i1;
s->next=g->adjlist[j1].firstedge;
g->adjlist[j1].firstedge=s;
}
}
return g;
}

int visited[max]; //标记是否访问

//深度优先遍历DFS
void dfs(linkG *g,int i) //顶点i
{
eNode *p;
printf("%c ",g->adjlist[i].vertex);
visited[i]=1;
p=g->adjlist[i].firstedge;
while(p)
{
if(!visited[p->adjvex])
dfs(g,p->adjvex);
p=p->next;
}

}

void dfstravel(linkG *g)
{
int i;
for(i=0;i<g->n;i++)
visited[i]=0;
for(i=0;i<g->n;i++)
if(!visited[i])
dfs(g,i);
}

//广度优先遍历BFS
void bfs(linkG *g,int i)
{
int j;
eNode *p;
int q[max],front,rear;
front=rear=0;
printf("%c ",g->adjlist[i].vertex);
visited[i]=1;
q[rear++]=i;
while(rear>front)
{
j=q[front++];
p=g->adjlist[j].firstedge;
while(p)
{
if(visited[p->adjvex]==0)
{
printf("%c ",g->adjlist[p->adjvex].vertex);
q[rear++]=p->adjvex;
visited[p->adjvex]=1;
}
p=p->next;
}
}
}

void bfstravel(linkG *g)
{
int i,count=0;
for(i=0;i<g->n;i++)
visited[i]=0;
for(i=0;i<g->n;i++)
if(!visited[i])
bfs(g,i);
}

//主函数
int main()
{
linkG *g;
g=creat(g,0);
printf("DFS:");
dfstravel(g);
printf("\n");
printf("BFS:");
bfstravel(g);
printf("\n");


}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Centos76配置lnmp(nginx116+php

发表于 2021-11-29

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

正文开始之前,先唠叨几句。

前段时间,博客遭到流量攻击,还好攻击流量不是很大,只有访客记录表的数据有些缺失。服务器现在用的是apache服务器,经受不住大流量的访问,服务器也快要到期了,新换一台服务器,重新配置一个nginx服务器吧。下边是我的血泪史。当然,我这只是能让我的网站在服务器上跑起来,在深入的运行权限之类的问题,目前可能涉及不到,以后遇到了再补充。

(一):安装nginx1.1.6

这个在LAMP环境配置及NGINX安装中介绍过,这里再介绍另一种方法,都大同小异,参照那个都可以。

1:安装nginx的yum源

1
bash复制代码rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

2:使用yum安装nginx

1
复制代码yum install -y nginx

3:启动nginx并设置开机自动运行

1
2
bash复制代码systemctl start nginx #启动,restart-重启,stop-停止
systemctl enable nginx #开机启动

4:卸载nginx

1
arduino复制代码yum remove nginx

5:查看nginx运行状态

1
lua复制代码systemctl  status  nginx

1.png

6:查看nginx版本

1
复制代码nginx -v

2.png

至此,nginx安装成功,在浏览器中输入你服务器的ip,就会出现如下界面。

3.png

最大的bug来了:(这是我配置的时候最大的bug)

配置nginx服务器成功之后,如果在浏览器输入服务器ip显示无法访问。

百思不得其姐。(难怪我单身)

百度了一下,没有找到答案,分开太麻烦了,寻思找个lnmp一件安装包装上试试,也不好用,最后发现,新买的服务器80端口没有开放。Nginx配置文件中,监听的是80端口,所以,一直不好用,开放特定端口,我之前日志中有写。大家可以去参照。至此,nginx安装完成。

(二):安装PHP7.2

我这里安装的是PHP7.2,如果你的项目能兼容的话,不建议安装PHP5.

1:安装PHP的yum源

1
2
ruby复制代码rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm

2:查看php7 yum组件

1
sql复制代码yum search php72w

3:选择自己需要的组件安装,php72w.x86_64 和 php72w-fpm.x86_64 为核心程序必装

1
复制代码yum install php72w.x86_64 php72w-fpm.x86_64 php72w-cli.x86_64 php72w-common.x86_64 php72w-gd.x86_64 php72w-ldap.x86_64 php72w-mbstring.x86_64 php72w-mcrypt.x86_64 php72w-mysql.x86_64 php72w-pdo.x86_64 php72w-pecl-redis.x86_64

4:启动php并设为开机启动

1
2
bash复制代码systemctl start php-fpm #启动,restart-重启,stop-停止
systemctl enable php-fpm #开机启动

5:查看版本及运行状态

1
2
perl复制代码php-fpm -v #查看版本
ps -ef | grep php-fpm #查看运行状态

PHP安装这个没什么可说的,一步一步安装就好了。

(三):修改nginx配置

这个需要修改nginx的配置文件nginx.conf

查找文件位置命令

1
复制代码whereis nginx.conf

修改这个有两个方式,如果你很了解linux操作命令,你可以使用vim直接在服务器上修改,不然,找到文件位置之后,将文件下载下来,在你本地修改完成之后,在传到服务器上。

1
arduino复制代码vi /etc/nginx/conf.d/default.conf

1:找到第一个location中的这一行

1
2
3
perl复制代码index  index.html index.htm;
修改为:
index  index.php index.html index.htm; #添加index.php

2:把FastCGI server这行下面的location的注释去掉,并修改成下面这样子

1
2
3
4
5
6
7
8
9
ini复制代码     # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
     #
     location ~ .php$ {
         root            /usr/share/nginx/html;  #网站根目录
         fastcgi_pass   127.0.0.1:9000;
         fastcgi_index  index.php;
         fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
         include        fastcgi_params;
     }
1
2
bash复制代码service nginx restart   #重启nginx
service php-fpm start   #开启php-fpm

3:在网站根目录新建index.php文件

1
bash复制代码vim /usr/share/nginx/html/index.php

随便写点什么内容都好,我这里输出的是phpinfo();

服务器ip/index.php 能访问,就说明nginx与 phpfpm配置成功。

(四):安装数据库maraidb10.3

(1)配置mariadb Yum源

vi /etc/yum.repos.d/MariaDB.repo  

在该文件中添加以下内容保存:

1
2
3
4
5
ini复制代码[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.2/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

(2)安装 MariaDB

1
vbscript复制代码yum install MariaDB-server MariaDB-client -y

(3)启动、停止服务

1
2
arduino复制代码systemctl start mariadb
systemctl stop mariadb

(4)设置开机启动

1
bash复制代码systemctl enable mariadb

(5)接下来进行MariaDB的相关简单配置

1
复制代码mysql_secure_installation

首先是设置密码,会提示先输入密码

1
rust复制代码Enter current password for root (enter for none):<–初次运行直接回车

设置密码

1
2
3
sql复制代码Set root password? [Y/n] <– 是否设置root用户密码,输入y并回车或直接回车
New password: <– 设置root用户的密码
Re-enter new password: <– 再输入一次你设置的密码

其他配置

1
2
3
4
bash复制代码Remove anonymous users? [Y/n] <– 是否删除匿名用户,回车
Disallow root login remotely? [Y/n] <–是否禁止root远程登录,回车,
Remove test database and access to it? [Y/n] <– 是否删除test数据库,回车
Reload privilege tables now? [Y/n] <– 是否重新加载权限表,回车

初始化MariaDB完成,接下来测试登录

1
复制代码mysql -uroot –ppassword

添加用户,设置权限

创建用户命令

1
sql复制代码mysql>create user username@localhost identified by 'password';

直接创建用户并授权的命令

1
css复制代码mysql>grant all on *.* to username@localhost indentified by 'password';

授予外网登陆权限

1
csharp复制代码mysql>grant all privileges on *.* to username@'%' identified by 'password';

授予权限并且可以授权

1
sql复制代码mysql>grant all privileges on *.* to username@'hostname' identified by 'password' with grant option;

刷新权限

1
arduino复制代码flush privileges; /*刷新权限*/

简单的用户和权限配置基本就这样了。

Maraidb安装完成。

如果安装完成之后没有mysql表 那就去/var/lib/ 把mysql文件夹删掉

至此,lnmp环境配置完成。

我就是这样配置的,我只是把在我服务器上配置的过程写了下来。

有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…127128129…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%